#!/usr/bin/perl -w
#
# Regina - A Normal Surface Theory Calculator
# Miscellaneous helper utility
#
# Copyright (c) 2023-2025, Ben Burton
# For further details contact Ben Burton (bab@debian.org).
#
# Usage: regina-helper <action> [args...]
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# As an exception, when this program is distributed through (i) the
# App Store by Apple Inc.; (ii) the Mac App Store by Apple Inc.; or
# (iii) Google Play by Google Inc., then that store may impose any
# digital rights management, device limits and/or redistribution
# restrictions that are required by its terms of service.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

use strict;
use Cwd 'abs_path';
use File::Basename;

# The program name and directory.
my $prog_name = $0;
my $prog_dir = abs_path(dirname($prog_name));

# Determine the operating system and installation type.
my $os = 'Linux';
my $install_type = 'XDG';
if ( -f "$prog_dir/cmake_install.cmake" and -f "$prog_dir/../CMakeCache.txt") {
    $install_type = 'Source';
}

my @final_messages = ();

# Extensions that the makefile should support:
my @cxx_extensions = ('cc', 'cpp');

#------------------------------------------------------------------------------
#  Helper functions
#------------------------------------------------------------------------------

sub usage {
    print STDERR <<__END__;
Usage: $prog_name <action> [args...]
       $prog_name <action> --help

Available actions:
    installtype : identify the type of Regina installation
    test        : run Regina's C++ test suite
    makefile    : write a Makefile for building C++ programs that use Regina
    cpp | cc    : write a sample C++ program that uses Regina, plus a Makefile
    help        : display this help
__END__
}

sub has_option {
    foreach my $arg (@ARGV) {
        foreach my $opt (@_) {
            $arg eq $opt and return 1;
        }
    }
    return 0;
}

sub add_final_message {
    my $msg = shift;
    foreach (@final_messages) {
        $msg eq $_ and return;
    }
    push @final_messages, $msg;
}

sub sanitise_for_makefile_recipe {
    # Returns the argument sanitised for use in an _unquoted_ context within a
    # Makefile recipe.
    my $arg = shift;
    $arg =~ s/\\/\\\\/g;
    $arg =~ s/([#'"`\$*?;&!()\[\]{}<>|~ \t])/\\$1/g;
    $arg =~ s/\$/\$\$/g;
    return $arg;
}

#------------------------------------------------------------------------------
#  Action: installtype
#------------------------------------------------------------------------------

sub installtype {
    if (has_option('--help')) {
        print STDERR <<__END__;
Usage: $prog_name installtype

Identifies the type of Regina installation.

Possible results:
    XDG     : Running from an installation in a fixed location that follows the
              freedesktop.org layout.  This is seen with Regina's GNU/Linux
              packages, as well as local CMake builds with no special arguments.
    HPC     : Running from a slimmed-down installation (e.g., no GUI or HTML
              docs) in a fixed but possibly non-standard location, suitable
              for builds on HPC systems.  This is typically seen with local
              CMake builds where REGINA_INSTALL_TYPE was explicitly set to HPC.
    Bundle  : Running from within Regina's macOS app bundle.  This is seen with
              Regina's macOS app, as well as local Xcode builds.
    Windows : Running from within Regina's Windows app.  This is seen with
              Regina's Windows app, as well as local builds that follow the
              (intricate and unsupported) msys2/mingw-w64 build process.
    Source  : Running directly from Regina's source tree (i.e., a local build
              that has not been installed).
__END__
        return 0;
    }

    print "$install_type\n";
    return 0;
}

#------------------------------------------------------------------------------
#  Action: test
#------------------------------------------------------------------------------

sub testsuite {
    # Here --help will print both our help and also the Google Test help.
    my $has_help = has_option('--help');
    if ($has_help) {
        print STDERR <<__END__;
Usage: $prog_name test [testsuite_args...]

Runs Regina's C++ test suite.

You can customise your test suite run by passing additional arguments that
are understood by the Google Test framework.  The possible arguments are
listed below.


__END__
        # Do not exit, since we want to run the test suite with --help.
    }

    my $srcDesc;
    my $testsuite;
    if ($install_type eq 'Source') {
        $srcDesc = "Running C++ test suite directly from the source tree.";
        $testsuite = "$prog_dir/testsuite/regina-testsuite";
    } elsif ($install_type eq 'Bundle') {
        $srcDesc = "Running C++ test suite from the macOS app bundle.";
        $testsuite = "$prog_dir/regina-testsuite";
    } else {
        $srcDesc = 'Running C++ test suite from the installation beneath ' .
            '/usr/.';
        $testsuite = '/usr/lib/regina' . '/regina-testsuite';
    }
    if (not -e $testsuite) {
        print STDERR "ERROR: Could not find test suite at: $testsuite\n";
        return 1;
    }
    my @cmdline = ($testsuite);
    if ($has_help) {
        # Ignore any other options; we will only display help.
        push @cmdline, '--help';
    } else {
        push @cmdline, @ARGV;
        print "$srcDesc\n\n";
    }
    if (not exec @cmdline) {
        print STDERR "ERROR: Could not execute test suite: $testsuite\n";
        return 1;
    }
    return 0;
}

#------------------------------------------------------------------------------
#  Action: makefile
#------------------------------------------------------------------------------

sub makefile {
    if (has_option('--help')) {
        print STDERR <<__END__;
Usage: $prog_name makefile [-f, --force] [-r, --rpath]

Writes a Makefile in the current directory for compiling C++ programs against
Regina.

Optional arguments:
    -f, --force : Overwrite any existing Makefile.
    -r, --rpath : Always include an rpath option in the Makefile (useful when
                  your Regina installation is not on the standard library path).
                  By default, an rpath option will only be added if you are
                  running directly out of Regina's source tree.
__END__
        return 0;
    }

    # If the installation does not support development then stop now.
    if ($install_type eq 'Windows') {
        print STDERR <<__END__;
ERROR: Cannot create a Makefile, since the Windows app for Regina does not ship
       with development files (i.e., Regina's C++ headers and related files).
__END__
        return 1;
    } elsif ($install_type eq 'Bundle') {
        print STDERR <<__END__;
ERROR: Cannot create a Makefile, since the macOS app for Regina does not ship
       with development files (i.e., Regina's C++ headers and related files).
__END__
        return 1;
    }

    if ((not has_option('-f', '--force')) and -e 'Makefile') {
        print STDERR "ERROR: Makefile already exists.\n";
        add_final_message('Use --force to overwrite existing files.');
        return 1;
    }

    # Do it!
    if (not open(MAKEFILE, '>', 'Makefile')) {
        print STDERR "ERROR: Could not write to Makefile.\n";
        return 1;
    }
    print STDERR "Preparing Makefile...\n";
    if ($install_type ne 'Source') {
        my $display_prefix = '/usr';
        my $make_regina_config = sanitise_for_makefile_recipe('/usr/bin/regina-engine-config');
        my $rpath_flags = '';
        if (has_option('-r', '--rpath')) {
            my $make_rpath_dir = sanitise_for_makefile_recipe('/usr/lib');
            $rpath_flags = "-Wl,-rpath $make_rpath_dir";
        }
        print MAKEFILE <<__END__;
# Compile C++ programs that use Regina.
# This uses the installation of Regina beneath $display_prefix/.
__END__
        foreach (@cxx_extensions) {
            print MAKEFILE <<__END__;

% : %.$_
	c++ -O3 '\$<' \`$make_regina_config --cflags --libs\` $rpath_flags -o '\$\@'
__END__
        }
    } else {
        # When running directly from the source tree, we cannot trust
        # regina-engine-config (since that outputs installation paths), and we
        # must always use rpath (since the source tree should not be on the
        # library path).
        my $include_flags = ' -I/usr/include -I/usr/include/libxml2 -I/usr/include -I/usr/include -I/usr/include';
        my $link_flags = ' /usr/lib/libxml2.so /usr/lib/libgmp.so /usr/lib/libgmpxx.so';
        $include_flags =~ s/\$/\$\$/g;
        $link_flags =~ s/\$/\$\$/g;

        my $make_prog_dir = sanitise_for_makefile_recipe($prog_dir);
        my $make_src_dir;
        if (-e "$prog_dir/../CMakeLists.txt") {
            $make_src_dir = "$make_prog_dir/..";
        } elsif (-e "$prog_dir/../../CMakeLists.txt") {
            $make_src_dir = "$make_prog_dir/../..";
        } else {
            print STDERR <<__END__;
ERROR: I could not deduce the top-level source directory.

Tried: $prog_dir/..
       $prog_dir/../..
__END__
            return 1;
        }
        print MAKEFILE <<__END__;
# Compile C++ programs that use Regina.
# This uses the build of Regina from $prog_dir/.
__END__

        foreach (@cxx_extensions) {
            print MAKEFILE <<__END__;

% : %.$_
	c++ -std=c++20 -O3 '\$<' \\
		-I$make_src_dir/engine -I$make_prog_dir \\
		$include_flags \\
		$make_prog_dir/libregina-engine.so \\
		$link_flags \\
		-Wl,-rpath $make_prog_dir \\
		-o '\$\@'
__END__
            if ($os eq 'Darwin') {
                # We need to put GMP on the rpath also, since this will not be
                # coming from a standard system installation.
                my $gmp = '/usr/lib/libgmp.so';
                my $gmpxx = '/usr/lib/libgmpxx.so';

                my ($gmp_dir, $gmpxx_dir);
                -e $gmp and $gmp_dir = abs_path(dirname($gmp));
                -e $gmpxx and $gmpxx_dir = abs_path(dirname($gmpxx));

                if ($gmp_dir) {
                    my $make_gmp_dir = sanitise_for_makefile_recipe($gmp_dir);
                    print MAKEFILE <<__END__;
		install_name_tool -add_rpath $make_gmp_dir '\$\@'
__END__
                }
                if ($gmpxx_dir and $gmpxx_dir ne $gmp_dir) {
                    my $make_gmpxx_dir = sanitise_for_makefile_recipe($gmpxx_dir);
                    print MAKEFILE <<__END__;
		install_name_tool -add_rpath $make_gmpxx_dir '\$\@'
__END__
                }
            }
        }
    }
    close MAKEFILE;
}

#------------------------------------------------------------------------------
#  Action: cpp | cc
#------------------------------------------------------------------------------

sub samplecode {
    # The preferred extension (e.g., cpp or cc) can be passed as an argument.
    my $extension = shift;
    $extension or $extension = 'cpp';

    if (has_option('--help')) {
        print STDERR <<__END__;
Usage: $prog_name { cpp | cc } [-f, --force] [-r, --rpath]

Writes a sample C++ program in the current directory that builds against Regina,
along with a corresponding Makefile.

The C++ filename extension will be whichever action you passed (cpp or cc).

Optional arguments:
    -f, --force : Overwrite any existing files.
    -r, --rpath : Always include an rpath option in the Makefile (useful when
                  your Regina installation is not on the standard library path).
                  By default, an rpath option will only be added if you are
                  running directly out of Regina's source tree.
__END__
        return 0;
    }

    my $filename = "sample.$extension";
    if ((not has_option('-f', '--force')) and -e $filename) {
        print STDERR "ERROR: $filename already exists.\n";
        add_final_message('Use --force to overwrite existing files.');
        return 1;
    }

    # Do it!
    if (not open(SOURCE, '>', $filename)) {
        print STDERR "ERROR: Could not write to $filename.\n";
        return 1;
    }
    print STDERR "Writing $filename...\n";
    print SOURCE <<__END__;
#include <iomanip>
#include <iostream>
#include "triangulation/dim3.h"
#include "triangulation/example3.h"

int main() {
    regina::Triangulation<3> tri = regina::Example<3>::weberSeifert();
    std::cout << tri.homology().str() << std::endl;
    return 0;
}
__END__
    close SOURCE;
    return 0;
}

#------------------------------------------------------------------------------
#  Option parsing and main code
#------------------------------------------------------------------------------

my $action = shift;
if (not defined $action) {
    usage();
    exit 1;
}
$action =~ tr/A-Z/a-z/;

my $retval = 0;
if ($action eq 'help' || $action eq '-h' || $action eq '--help') {
    usage();
} elsif ($action eq 'installtype') {
    $retval |= installtype();
} elsif ($action eq 'test') {
    $retval |= testsuite();
} elsif ($action eq 'makefile') {
    $retval |= makefile();
} elsif ($action eq 'cpp' || $action eq 'cc') {
    $retval |= samplecode($action);
    # If we are just asking for help, don't output the makefile help also.
    has_option('--help') or $retval |= makefile();
} else {
    print STDERR "ERROR: Unknown action ($action).\n\n";
    usage();
    exit 1;
}

if (@final_messages) {
    print STDERR "\n";
    foreach (@final_messages) {
        print STDERR "$_\n";
    }
}

exit $retval;
