# SPDX-License-Identifier: BSD-3-Clause-CMU
# See COPYING file at the root of the distribution for more details.
package Cyrus::IMAPOptions::App::Command::csource;
use warnings;
use strict;

use experimental 'signatures';

use Cyrus::IMAPOptions::App -command;

sub abstract () { 'build c source file from imapoptions' }

sub usage_desc (@) { 'csource %o imapoptions' }

sub opt_spec (@) { return Cyrus::IMAPOptions::App::common_opt_spec; }

sub validate_args ($self, $opt, $args)
{
    $self->usage_error("need imapoptions path") if @$args < 1;
    $self->usage_error("too many arguments") if @$args > 1;

    $self->usage_error("imapoptions must be a directory")
        if ! -d $args->[0];
}

sub execute ($self, $opt, $args)
{
    my $imapoptions = Cyrus::IMAPOptions->new(
        from_path => $args->[0],
        forbid_unreleased => $opt->forbid_unreleased,
    );

    $imapoptions->check_unreleased($self->app->arg0);

    $self->header();
    $imapoptions->iterate(\&_print_option, $opt);
    $self->footer();
}

sub header ($self)
{
    my $c = <<~'END_HEADER';
    /* auto-generated by tools/imapoptions.pl */
    #include <config.h>
    #include <sys/types.h>
    #ifdef HAVE_UNISTD_H
    #include <unistd.h>
    #endif
    #include <stdlib.h>
    #include <string.h>
    #include "imapopts.h"

    EXPORTED struct imapopt_s imapopts[] =
    {
        {   .opt = IMAPOPT_ZERO,
            .name = "",
            .seen = 0,
            .type = OPT_NOTOPT,
            .last_modified = 0,
            .deprecated_since = NULL,
            .replaced_by = IMAPOPT_ZERO,
            .val.s = NULL,
            .def.s = NULL,
            .defunit = 0,
            .enum_options = {
                { NULL, IMAP_ENUM_ZERO },
            },
        },
    END_HEADER

    print $c;
}

sub footer ($self)
{
    my $c = <<~'END_FOOTER';
        {   .opt = IMAPOPT_LAST,
            .name = NULL,
            .seen = 0,
            .type = OPT_NOTOPT,
            .last_modified = 0,
            .deprecated_since = NULL,
            .replaced_by = IMAPOPT_ZERO,
            .val.s = NULL,
            .def.s = NULL,
            .defunit = 0,
            .enum_options = {
                { NULL, IMAP_ENUM_ZERO },
            },
        },
    };

    END_FOOTER

    print $c;
}

sub _print_struct_field ($key, $value)
{
    print "        .$key = $value,\n";
}

sub _print_option ($name, $option, $opt)
{
    return if $option->for_documentation_only;

    print '    {   .opt = ' . $option->c_name . ",\n";
    _print_struct_field('name', '"' . $option->name . '"');
    _print_struct_field('seen', '0');
    _print_struct_field('type', 'OPT_' . $option->type);
    _print_struct_field('last_modified', $option->c_last_modified);
    _print_struct_field('deprecated_since', $option->c_deprecated_since);
    _print_struct_field('replaced_by', $option->c_replaced_by);

    # We write the default value twice:
    # The first initialises the 'val' field of the struct, which is what makes
    # this the default value.  The second sets the 'def' field of the struct,
    # which libconfig uses to see when val has been changed from the default.
    my ($union_field, $default_value) = $option->c_default_value;
    foreach my $k ('val', 'def') {
        _print_struct_field("$k.$union_field", $default_value);
    }
    _print_struct_field('defunit', $option->c_default_unit);

    print "        .enum_options = {\n";
    foreach my $pair ($option->c_allowed_values) {
        my ($v, $e) = @$pair;
        print "            { \"$v\", $e },\n";
    }
    print "            { NULL, IMAP_ENUM_ZERO },\n";
    print "        },\n";
    print "    },\n";
}

1;
