#! /usr/bin/perl

##  Generate linkage code and makefiles for storage and overview methods.
##
##  Goes through all subdirectories of the current directory and finds
##  directories that are either storage methods or overview methods.  Builds
##  methods.[ch] and ovmethods.[ch] as well as makefile stubs.
##
##  Created by James Brister in 1998.
##  Various bug fixes, code and documentation improvements since then
##  in 1998-2002, 2004, 2009, 2020-2022, 2024, 2025.

require 5.003;

use strict;
use vars qw(@OVERVIEW @STORAGE);

# Storage API functions.
@STORAGE = qw(
    init store retrieve next freearticle cancel ctl flushcacheddata
    printfiles explaintoken shutdown
);

# Overview API functions.
@OVERVIEW = qw(
    open groupstats groupadd groupdel add cancel opensearch search
    closesearch getartinfo expiregroup ctl close
);

my (%filelistix, @filelistnames, $filelistparam);

BEGIN {
    # All known file lists.
    # (*.config param name => Make.methods variable name)
    my @filelists = (
        sources         => "METHOD_SOURCES",
        "extra-sources" => "EXTRA_SOURCES",
        programs        => "PROGRAMS",
        clean           => "RM_CLEAN",
        distclean       => "RM_DISTCLEAN",
        maintclean      => "RM_MAINTCLEAN",
    );
    %filelistix = map({ ($filelists[$_ * 2] => $_) } 0 .. @filelists / 2 - 1);
    @filelistnames = map({ $filelists[$_ * 2 + 1] } 0 .. @filelists / 2 - 1);
    $filelistparam = join("|", map({ quotemeta($_) } keys(%filelistix)));
}

# Used to make heredocs more readable.
sub unquote { my ($string) = @_; $string =~ s/^:( {0,7}|\t)//gm; $string }

# Parse a method.config file for a storage method, putting information about
# that storage method into the given hash ref.
sub parse_config {
    my ($dir, $file, $config, $makefile) = @_;
    local $_;
    open(CONFIG, "$dir/$file") or die "Can't open $dir/$file: $!\n";
    while (<CONFIG>) {
        s/^\s+//;
        s/\s+$//;
        if (/^name\s*=\s*(\S+)$/) {
            my $method = $1;
            die "$dir/$file: $method has already been defined\n"
              if (defined $$config{method}{$method});
            $$config{method}{$method} = $dir;
        } elsif (/^number\s*=\s*(\d+)$/) {
            my $number = $1;
            if (defined $$config{number}{$number}) {
                die "$dir/$file: method number $number was already "
                  . "allocated in $$config{number}{$number}\n";
            }
            $$config{number}{$dir} = $number;
        } elsif (/^($filelistparam)\s*=\s*(.*)/) {
            my $ix = $filelistix{$1};
            my $files = $2;
            my @files = sort(split(" ", $files));
            push(@{ $$makefile{filelists}[$ix] }, map({ "$dir/$_" } @files));
        } else {
            warn "$dir/$file: ignoring unknown line: $_\n";
        }
    }

    # If there is a makefile fragment in the directory, note it.
    if (-f "$dir/method.mk") {
        push(@{ $$makefile{makefiles} }, "$dir/method.mk");
    } elsif (-f "$dir/ovmethod.mk") {
        push(@{ $$makefile{makefiles} }, "$dir/ovmethod.mk");
    }
    push(@{ $makefile->{dirs} }, $dir);
}

# Write out include directives for a list of files.
sub write_includes {
    my ($fh, $config) = @_;
    for my $method (sort keys %{ $$config{method} }) {
        my $path = $$config{method}{$method};
        print $fh qq(#include "$path/$method.h"\n);
    }
}

# Write out the method struct.
sub write_methods {
    my ($fh, $config, $prefix, @funcs) = @_;
    my $notfirst;
    for my $method (sort keys %{ $$config{method} }) {
        print $fh "\n},\n" if $notfirst;
        print $fh qq({\n    "$method");
        print $fh ', ', $prefix, '_', uc($method) if $prefix;
        for (@funcs) {
            print $fh ",\n    ${method}_$_";
        }
        $notfirst++;
    }
    print $fh "\n}\n};\n\n";
}

# Write out the constant defines for methods.
sub write_constants {
    my ($fh, $config, $prefix) = @_;
    for my $method (sort keys %{ $$config{method} }) {
        printf $fh "#define ${prefix}_%-30s%d\n", uc($method),
          $$config{number}{ $$config{method}{$method} };
    }
}

# Write out methods.c and methods.h for the interface to the storage
# methods.
sub write_storage {
    my $storage = shift;
    open(DEF, '> methods.c.new') or die "Can't create methods.c.new: $!\n";
    print DEF unquote(<<'EOE');
:       /* This file is automatically generated by buildconfig. */
:
:       #include "interface.h"
:       #include "methods.h"
:
EOE
    write_includes(\*DEF, $storage);
    print DEF "\nSTORAGE_METHOD storage_methods[",
      scalar(keys %{ $$storage{method} }), "] = {\n";
    write_methods(\*DEF, $storage, 'TOKEN', @STORAGE);
    close DEF;
    rename('methods.c.new', 'methods.c');

    open(H, '> methods.h.new') or die "Can't open methods.h.new: $!\n";
    print H unquote(<<'EOE');
:       /* This file is automatically generated by buildconfig. */
:
:       #ifndef METHODS_H
:       #define METHODS_H 1
:
:       #include "interface.h"
:
EOE
    print H '#define NUM_STORAGE_METHODS ',
      scalar(keys %{ $$storage{method} }), "\n\n";
    write_constants(\*H, $storage, 'TOKEN');
    print H unquote(<<'EOE');
:
:       extern STORAGE_METHOD storage_methods[NUM_STORAGE_METHODS];
:
:       #endif /* METHODS_H */
EOE
    close H;
    rename('methods.h.new', 'methods.h');
}

# Write out ovmethods.c and ovmethods.h for the interface to the overview
# methods.
sub write_overview {
    my $overview = shift;
    open(DEF, '> ovmethods.c.new')
      or die "Can't create ovmethods.c.new: $!\n";
    print DEF unquote(<<'EOE');
:       /* This file is automatically generated by buildconfig. */
:
:       #include "ovinterface.h"
:       #include "ovmethods.h"
:
EOE
    write_includes(\*DEF, $overview);
    print DEF "\nOV_METHOD ov_methods[",
      scalar(keys %{ $$overview{method} }), "] = {\n";
    write_methods(\*DEF, $overview, undef, @OVERVIEW);
    close DEF;
    rename('ovmethods.c.new', 'ovmethods.c');

    open(H, '> ovmethods.h.new') or die "Can't open ovmethods.h.new: $!\n";
    print H unquote(<<'EOE');
:       /* This file is automatically generated by buildconfig. */
:
:       #ifndef OVMETHODS_H
:       #define OVMETHODS_H 1
:
:       #include "ovinterface.h"
:
EOE
    print H '#define NUM_OV_METHODS ',
      scalar(keys %{ $$overview{method} }), "\n";
    print H unquote(<<'EOE');
:
:       extern OV_METHOD ov_methods[NUM_OV_METHODS];
:
:       #endif /* OVMETHODS_H */
EOE
    close H;
    rename('ovmethods.h.new', 'ovmethods.h');
}

# Return a string setting a makefile variable.  Tab over the = properly and
# wrap to fit our coding standards.
sub makefile_var {
    my ($variable, @values) = @_;
    my $output;
    $output = sprintf("%-15s =", $variable);
    my $column = 17;
    for (@values) {
        if ($column > 17 && 77 - $column < length($_)) {
            $output .= " \\\n" . ' ' x 17;
            $column = 17;
        }
        $output .= " $_";
        $column += 1 + length($_);
    }
    $output .= "\n";
    return $output;
}

# Write out the makefile fragment for overview and storage methods.
sub write_makefile {
    my ($makefile) = @_;
    open(MAKE, '> Make.methods.new')
      or die "Can't create Make.methods.new: $!\n";
    print MAKE "# This file is automatically generated by buildconfig.\n\n";
    for (0 .. $#filelistnames) {
        print MAKE makefile_var(
            $filelistnames[$_],
            @{ $makefile->{filelists}[$_] },
        );
    }
    for (@{ $makefile->{makefiles} }) {
        print MAKE "\n\n##  Included from $_\n\n";
        open(FRAG, $_) or die "Can't open $_: $!\n";
        print MAKE <FRAG>;
        close FRAG;
    }
    rename('Make.methods.new', 'Make.methods');
}

my (%storage, %overview, %makefile);
%makefile = (
    filelists => [map({ [] } @filelistnames)],
    makefiles => [],
    dirs      => [],
);
if (!-d 'cnfs') {
    if (-d 'storage/cnfs') {
        chdir 'storage' or die "Can't chdir to storage: $!\n";
    } else {
        die "Can't find storage directory (looking for storage/cnfs)\n";
    }
}
opendir(D, ".") or die "Can't open current directory: $!\n";
my @dirs = sort readdir D;
for my $dir (@dirs) {
    if (-e "$dir/method.config") {
        parse_config($dir, 'method.config', \%storage, \%makefile);
    } elsif (-e "$dir/ovmethod.config") {
        parse_config($dir, 'ovmethod.config', \%overview, \%makefile);
    }
}
write_storage(\%storage);
write_overview(\%overview);
write_makefile(\%makefile);
