#!/usr/bin/perl
#
# Copyright (c) 1994-2019 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.

require 5;

use strict;
use warnings;

# invoked by make like this:
#./tools/config2header CC="gcc" ./lib/imapopts.c ./lib/imapopts.h < ./lib/imapoptions

my $enum_size = 0;
my @enum_values = ();

my %vars;
my %known_vars = map { $_ => 1 } qw(CC);
my %opts;
my %known_opts = map { $_ => 1 } qw(forbid-unreleased);

# process command line arguments
while (scalar @ARGV) {
    if ($ARGV[0] =~ m{^(\S+)=(.*)$}) {
        # var=value
        die "unrecognised variable $1" if not exists $known_vars{$1};
        die "missing value for $1" if not defined $2;
        $vars{$1} = $2;
        shift @ARGV;
    }
    elsif ($ARGV[0] =~ m{^--([-\w]+)(?:=(.*))?$}) {
        # --option or --option=value
        die "unrecognised option $ARGV[0]" if not exists $known_opts{$1};
        $opts{$1} = $2 // 1;
        shift @ARGV;
    }
    else {
        last;
    }
}

my $use_gcc_extension = (exists $vars{CC} and $vars{CC} eq 'gcc');

die "wrong number of arguments" if scalar @ARGV != 2;
my ($cfile, $hfile) = @ARGV;

open CFILE, ">$cfile";
open HFILE, ">$hfile";

my $blank = "";
# XXX clean up old cvs id cruft!
my $version = "\$Revision: 1.17 $blank";
$version =~ s/.Revision: (.*) /$1/;
print HFILE "/* auto-generated by config2header $version */\n";
print CFILE "/* auto-generated by config2header $version */\n";

print HFILE "#ifndef INCLUDED_IMAPOPTS_H\n";
print HFILE "#define INCLUDED_IMAPOPTS_H\n";
print HFILE "\n";
print HFILE "#include <stdint.h>\n";
print HFILE "\n";

# prototypes
my @opttype = ("OPT_NOTOPT", "OPT_STRING", "OPT_INT", "OPT_SWITCH",
               "OPT_ENUM", "OPT_STRINGLIST", "OPT_BITFIELD", "OPT_DURATION",
               "OPT_BYTESIZE");
print HFILE "enum opttype {\n";
while (my $opt = pop (@opttype)) {
    if ($#opttype == -1) {
      print HFILE "  $opt\n";
    } else {
      print HFILE "  $opt,\n";
    }
}
print HFILE "};\n\n";

print HFILE <<EOF
enum imapopt {
  IMAPOPT_ZERO = 0,
EOF
    ;

print CFILE <<EOF
/* DO NOT EDIT */
/* THIS FILE AUTOMATICALLY GENERATED BY config2header $version */
#include <config.h>
#include <sys/types.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdlib.h>
#include <string.h>
#include "imapopts.h"

/*
 * Sun C Compilers are more strict than GNU and won't allow type
 * casting to a union
 */

#if defined(__SUNPRO_C) || defined(__SUNPRO_CC)
#define U_CFG_V
#else
#define U_CFG_V (union config_value)
#endif


EXPORTED struct imapopt_s imapopts[] =
{
  { IMAPOPT_ZERO, "", 0, OPT_NOTOPT, 0, NULL, IMAPOPT_ZERO, { NULL }, { NULL }, { { NULL, IMAP_ENUM_ZERO } } },
EOF
    ;

my $__warned_unreleased = 0;
sub parse_last_modified
{
    my ($version) = @_;

    if ($version =~ m{ ^ (\d+) \. (\d+) \. (\d+) $ }x) {
        my ($maj, $min, $rev) = (0 + $1, 0 + $2, 0 + $3);

        die "major version too large: $maj" if $maj > 255;
        die "minor version too large: $min" if $min > 255;
        die "revision too large: $rev" if $rev > 255;

        return sprintf "0x%2.2X%2.2X%2.2X00", $maj, $min, $rev;
    }
    elsif ($version eq 'UNRELEASED') {
        if (not $__warned_unreleased) {
            # This warning is to remind the release manager to replace
            # "UNRELEASED" strings in lib/imapoptions with the version
            # number that is about to be released.
            # If you're not building a release, ignore it. :)
            my $prefix;

            if ($opts{'forbid-unreleased'}) {
                $prefix = -t STDERR ? "\033[31;1merror:\033[0m" : 'error:';
            }
            else {
                $prefix = -t STDERR ? "\033[33;1mwarning:\033[0m" : 'warning:',
            }

            my $w = join q{ },
                    "$0:",
                    $prefix,
                    'build contains UNRELEASED config options';
            print STDERR "$w\n";

            $__warned_unreleased ++;

            exit 1 if $opts{'forbid-unreleased'};
        }

        return "0xFFFFFFFF";
    }
    else {
        die "unparseable version: $version";
    }
}


# output enumeration

while (<STDIN>) {
    next if (/^\#/);
    # look for { option, default, type [enums] }

    # Note that the code we output has to play interesting games to get
    # the union to initialize itself in a syntactically valid manner.
    # Namely, we need to initialize the union itself, not the members of
    # the union, and we need to ensure we are initializing the union with
    # something of a type that is in the union.
    if (m|
            {           # opening curly
            \s*         # skip leading whitespace
            \"(.*?)\"   # $1: option name (don't capture quotes)
            \s*,\s*     # comma, optional whitespace
            (\"?.*?\"?) # $2: default value (do capture quotes)
            \s*,\s*     # comma, optional whitespace
            (.*?)       # $3: option type
            \s*         # optional whitespace
            (\(.*\))?   # $4: list of permitted values, n.b. perl syntax!
            \s*,\s*     # comma, optional whitespace
            \"([^\"]+)\"# $5: last modified version (don't capture quotes)
            \s*         # optional whitespace
            (,.*)?      # $6: deprecation info
            }           # closing curly
        |x) {
        my $opt = $1;
        my $def;
        my $depver = "NULL";
        my $newopt = "ZERO";
        my $enums = "";
        my $lastmod = parse_last_modified($5);

        print HFILE "  IMAPOPT_", uc($opt), ",\n";

        if ($3 eq "STRING") {
            $def = $use_gcc_extension
                        ? "U_CFG_V((const char *) $2)"
                        : "{(void *)($2)}";
        } elsif ($3 eq "ENUM") {
            my @values = eval $4;
            my $e;
            my $v;
            my $count = 0;

            # strip quotes from default value
            $def = substr($2, 1, -1);
            $e = "IMAP_ENUM_" . uc($opt) . "_" . uc($def);
            $e =~ s/[^0-9A-Z_a-z]/_/g;
            $def = $use_gcc_extension
                        ? "U_CFG_V((enum enum_value) $e)"
                        : "{(void *)($e)}";

            # output the enum_options
            foreach $v (@values) {
                $e = "IMAP_ENUM_" . uc($opt) . "_" . uc($v);
                $e =~ s/[^0-9A-Z_a-z]/_/g;
                $enums .= " { \"$v\" , $e },\n     ";

                # if this is the first enum value, normalize to zero
                if (!$count) { $e .= " = 0"; }

                # add this enum to enum_values
                push(@enum_values, $e);

                $count += 1;  # count the number of values
            }

            # [re]set the size of the enum_options array
            if ($count > $enum_size) { $enum_size = $count; }
        } elsif ($3 eq "STRINGLIST") {
            my @values = eval $4;
            my $v;
            my $count = 0;

            $def = $use_gcc_extension
                        ? "U_CFG_V((const char *) $2)"
                        : "{(void*)($2)}";

            # output the enum_options
            foreach $v (@values) {
                $enums .= " { \"$v\" , IMAP_ENUM_ZERO },\n     ";
                $count += 1;  # count the number of values
            }

            # [re]set the size of the enum_options array
            if ($count > $enum_size) { $enum_size = $count; }
        } elsif ($3 eq "BITFIELD") {
            my @values;
            my $e;
            my $v;
            my $count = 0;
            my $bit = 0;

            # strip quotes from default value
            $def = substr($2, 1, -1);

            # build the bitwise-or of the defaults
            @values = split(' ', $def);
            $e = "";
            foreach $v (@values) {
                my $ev = "IMAP_ENUM_" . uc($opt) . "_" . uc($v);
                $ev =~ s/[^0-9A-Z_a-z]/_/g;
                $e .= "$ev\n\t\t\t | ";
            }
            $e .= "0";
            $def = $use_gcc_extension
                        ? "U_CFG_V((uint64_t) $e)"
                        : "{(void *)($e)}";

            # output the enum_options
            @values = eval $4;
            foreach $v (@values) {
                # split any aliases
                my @aliases = split('=', $v);

                # the first option is used for the value
                $v = $aliases[0];

                $e = "IMAP_ENUM_" . uc($opt) . "_" . uc($v);
                $e =~ s/[^0-9A-Z_a-z]/_/g;

                # assign all aliases the same value
                foreach $a (@aliases) {
                    $enums .= " { \"$a\" , $e },\n     ";
                    $count += 1;  # count the number of options
                }

                # add the corresponding bit value
                $e .= " = (UINT64_C(1)<<$bit)";

                # add this enum to enum_values
                push(@enum_values, $e);

                $bit += 1;  # bump the bit
            }

            # [re]set the size of the enum_options array
            if ($count > $enum_size) { $enum_size = $count; }
        } elsif ($3 eq "DURATION") {
            $def = $use_gcc_extension
                        ? "U_CFG_V((const char *) $2)"
                        : "{(void *)($2)}";
        } elsif ($3 eq "BYTESIZE") {
            $def = $use_gcc_extension
                        ? "U_CFG_V((const char *) $2)"
                        : "{(void *)($2)}";
        } else {
            $def = $use_gcc_extension
                        ? "U_CFG_V((long) $2)"
                        : "{(void*)$2}";
        }

        if ($6) {
            # option is deprecated
            if ($6 =~ m|
                    ,\s*            # comma and optional whitespace
                    \"([^,]+)\"     # $1: 'deprecated since' version string
                    \s*             # optional whitespace
                    (               # $2: (unused)
                        ,\s*        # comma and optional whitespace
                        \"(.+)\"    # $3: 'in favour of' option name
                    )?
                |x) {
                $depver = qq{"$1"};
                $newopt = $3 if $3;

                # we don't use the parsed value here, but we do still want to
                # detect and report if "UNRELEASED" is seen
                (undef) = parse_last_modified($1);
            } else {
                #chomp;
                #print "rejected '$6'\n";
            }
        }

        print CFILE "  { IMAPOPT_", uc($opt), ", \"$1\", 0, OPT_$3, $lastmod,\n",
        "    $depver, IMAPOPT_", uc($newopt), ",\n",
        "    $def,\n    $def,\n    {$enums { NULL, IMAP_ENUM_ZERO } } },\n";
    } else {
        #chomp;
        #print "rejected '$_'\n";
    }
}

print HFILE <<EOF
  IMAPOPT_LAST
};

enum enum_value {
  IMAP_ENUM_ZERO = 0,
EOF
;

# add the enum_values
while (my $e = shift (@enum_values)) {
    if ($#enum_values == -1) {
      print HFILE "  $e\n";
    } else {
      print HFILE "  $e,\n";
    }
}

my $dummy_field = $use_gcc_extension ? '' : 'void *dummy;';
print HFILE <<EOF
};

union config_value {
    $dummy_field
    const char *s;      /* OPT_STRING, OPT_STRINGLIST, OPT_DURATION, OPT_BYTESIZE */
    long i;             /* OPT_INT */
    long b;             /* OPT_SWITCH */
    enum enum_value e;  /* OPT_ENUM */
    uint64_t x;         /* OPT_BITFIELD */
};

struct enum_option_s {
    const char *name;
    const enum enum_value val;
};

#define MAX_ENUM_OPTS $enum_size
struct imapopt_s {
    const enum imapopt opt;
    const char *optname;
    int seen;
    const enum opttype t;
    uint32_t last_modified;
    const char *deprecated_since;
    const enum imapopt preferred_opt;
    union config_value val;
    const union config_value def;
EOF
;

print HFILE "    const struct enum_option_s enum_options[MAX_ENUM_OPTS+1];\n";

print HFILE <<EOF
};

extern struct imapopt_s imapopts[];

#endif /* INCLUDED_IMAPOPTIONS_H */
EOF
    ;

print CFILE <<EOF
  { IMAPOPT_LAST, NULL, 0, OPT_NOTOPT, 0, NULL, IMAPOPT_ZERO, { NULL }, { NULL }, { { NULL, IMAP_ENUM_ZERO } } }
};

/* c code goes here */

EOF
;
