#!/usr/bin/perl
# promdatagen - generate promdata.[ch] files

# Copyright (c) 1994-2017 Carnegie Mellon University.  All rights reserved.

# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:

# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.

# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.

# 3. The name "Carnegie Mellon University" must not be used to
#    endorse or promote products derived from this software without
#    prior written permission. For permission or any legal
#    details, please contact
#      Carnegie Mellon University
#      Center for Technology Transfer and Enterprise Creation
#      4615 Forbes Avenue
#      Suite 302
#      Pittsburgh, PA  15213
#      (412) 268-7393, fax: (412) 268-7395
#      innovation@andrew.cmu.edu

# 4. Redistributions of any form whatsoever must retain the following
#    acknowledgment:
#    "This product includes software developed by Computing Services
#     at Carnegie Mellon University (http://www.cmu.edu/computing/)."

# CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
# FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
# AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
# OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

use warnings;
use strict;

use Data::Dumper;
use Getopt::Std;

my %types = ( counter => 'PROM_METRIC_COUNTER', gauge => 'PROM_METRIC_GAUGE' );

my %options;
my @metrics;
my @labels;

sub output_header;
sub output_source;

die "usage\n" if not getopts("h:c:v", \%options);

my $lineno = 0;
while (my $line = <>) {
    $lineno ++;

    chomp $line;
    $line =~ s{#.*$}{};             # eat comments
    $line =~ s{\s+$}{};             # eat trailing whitespace
    next if $line =~ m{^\s*$};      # eat empty lines

    if ($line =~ m{^\s*metric\s}) {
        # parse a metric:
        # metric counter imap_connections_total  The total number of IMAP connections
        $line =~ s{^\s*metric\s+}{};
        my ($type, $name, $help) = split /\s+/, $line, 3;
    
        if (not exists $types{$type}) {
            die "\"$type\" is not a valid type at line $lineno\n";
        }

        if ($name !~ m{^[a-z][a-z0-9_]*$}) {
            die "\"$name\" is not a valid metric name at line $lineno\n";
        }

        push @metrics, { type => $type, name => $name, help => $help };

    }
    elsif ($line =~ m{^\s*label\s}) {
        # parse a label:
        # label imap_authenticate_count result yes no
        $line =~ s{^\s*label\s+}{};
        my ($name, $label, @values) = split /\s+/, $line;

        if (not scalar grep { $_->{name} eq $name } @metrics) {
            die "cannot define label \"$label\" for unknown metric \"$name\" at line $lineno\n";
        }

        if ($label !~ m{^[a-z][a-z0-9_]*$}) {
            die "\"$label\" is not a valid label at line $lineno\n";
        }

        foreach my $v (@values) {
            if ($v !~ m{^[a-z][a-z0-9_]*$}) {
                die "\"$v\" is not a valid value at line $lineno\n";
            }
        }

        foreach my $metric (@metrics) {
            if ($metric->{name} eq $name) {
                if (exists $metric->{label} ) {
                    die "cannot define more than one label for metric \"$name\" at line $lineno\n";
                }
                $metric->{label} = { name => $name, label => $label, values => [ @values ] };
                push @labels, $metric->{label};
                last;
            }
        }
    }
    else {
        warn "skipping unparseable line at line $lineno: $line\n";
        next;
    }
}

output_header($options{h}, \@metrics, \@labels) if $options{h};
output_source($options{c}, \@metrics, \@labels) if $options{c};

exit 0;

sub output_header
{
    my ($fname, $metrics, $labels) = @_;

    open my $header, '>', $fname or die "$fname: $!\n";
    print $header "#ifndef INCLUDE_PROMDATA_H\n#define INCLUDE_PROMDATA_H\n";
    print $header "/* generated from $ARGV */\n";

    print $header <<OKAY;

#include <sys/types.h>

#include <stdint.h>

enum prom_metric_type {
    PROM_METRIC_COUNTER   = 0,
    PROM_METRIC_GAUGE     = 1,
    PROM_METRIC_HISTOGRAM = 2, /* unused */
    PROM_METRIC_SUMMARY   = 3, /* unused */
    PROM_METRIC_CONTINUED = 4, /* internal use only */
};
extern const char *prom_metric_type_names[];

OKAY

    print $header "enum prom_metric_id {\n";
    my $first = 1;
    foreach my $metric (@{$metrics}) {
        if (exists $metric->{label}) {
            foreach my $v (@{$metric->{label}->{values}}) {
                print $header "    \U$metric->{name}_$metric->{label}->{label}_$v\E";
                print $header q{ = 0} if $first;
                $first = 0;
                print $header qq{,\n};
            }
        }
        else {
            print $header q{    }, uc($metric->{name});
            print $header q{ = 0} if $first;
            $first = 0;
            print $header qq{,\n};
        }
    }
    print $header "\n    PROM_NUM_METRICS /* n.b. leave last! */\n";
    print $header "};\n\n";

    print $header "enum prom_labelled_metric {\n";
    $first = 1;
    foreach my $label (@{$labels}) {
        print $header "    \U$label->{name}\E";
        print $header q{ = 0} if $first;
        $first = 0;
        print $header qq{,\n};
    }
    print $header "\n    PROM_NUM_LABELLED_METRICS /* n.b. leave last! */\n";
    print $header "};\n";

print $header <<OKAY;

struct prom_label_lookup_value {
    const char *value;
    enum prom_metric_id id;
};

extern const struct prom_label_lookup_value *prom_label_lookup_table[];

OKAY

    print $header <<OKAY;

struct prom_metric_desc {
    const char *name;
    enum prom_metric_type type;
    const char *help;
    const char *label;
};
extern const struct prom_metric_desc prom_metric_descs[];

struct prom_metric {
    double value;
    int64_t last_updated;
};

struct prom_stats {
    char  ident[512];   /* XXX places upper limit on service names */
    struct prom_metric metrics[PROM_NUM_METRICS];
};
#define PROM_STATS_INITIALIZER { {0}, {{0, 0}} }

OKAY

    print $header "#endif\n";
    close $header;
}

sub output_source
{
    my ($fname, $metrics, $labels) = @_;

    open my $source, '>', $fname or die "$fname: $!\n";

    print $source <<OKAY;
/* generated from $ARGV */

#include <config.h>

#include "imap/promdata.h" /* XXX */

EXPORTED const char *prom_metric_type_names[] = {
    "counter",
    "gauge",
    "histogram",
    "summary",
    NULL,
};

OKAY

    print $source "EXPORTED const struct prom_metric_desc prom_metric_descs[] = {\n";
    foreach my $metric (@{$metrics}) {
        if (exists $metric->{label}) {
            my $first = 1;
            foreach my $v (@{$metric->{label}->{values}}) {
                printf $source '    { "%s", %s, ',
                            $metric->{name},
                            ($first ? $types{$metric->{type}} : "PROM_METRIC_CONTINUED");
                if ($first && defined $metric->{help}) {
                    printf $source '"%s", ', $metric->{help};
                }
                else {
                    print $source "NULL, ";
                }
                printf $source '"%s=\\"%s\\""', $metric->{label}->{label}, $v;
                print $source " },\n";
                $first = 0;
            }
        }
        else {
            printf $source '    { "%s", %s, ',
                        $metric->{name},
                        $types{$metric->{type}};
            if (defined $metric->{help}) {
                printf $source '"%s",', $metric->{help};
            }
            else {
                print $source "NULL,";
            }
            print $source " NULL },\n";
        }
    }
    print $source "    { NULL, 0, NULL, NULL },\n";
    print $source "};\n\n";

    foreach my $label(@{$labels}) {
        print $source "static const struct prom_label_lookup_value ";
        print $source "\U$label->{name}_$label->{label}\E_values[] = {\n";
        foreach my $value(sort @{$label->{values}}) {
            print $source "    { \"$value\", \U$label->{name}_$label->{label}_$value\E },\n";
        }
        print $source "    { NULL, 0 },\n";
        print $source "};\n\n";
    }

    print $source "EXPORTED const struct prom_label_lookup_value *prom_label_lookup_table[] = {\n";
    foreach my $label (@{$labels}) {
        print $source "    \U$label->{name}_$label->{label}\E_values,\n";
    }
    print $source "};\n";

    close $source;
}
