#!/usr/bin/env perl

# Script to create a configuration file for kdesrc-build.
#
# Copyright © 2011, 2020 Michael Pyne. <mpyne@kde.org>
# Home page: https://kdesrc-build.kde.org/
#
# 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.
#
# 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, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

use strict;
use 5.018;
use IO::Pipe;
use File::Copy;
use File::Temp qw/tempfile/;
use File::Basename;
use Cwd qw(abs_path);

our $VERSION = 0.03; # Not user-visible yet.

sub clearScreen
{
    require POSIX;
    my $termios = POSIX::Termios->new();
    $termios->getattr(1); # Get STDOUT attributes

    require Term::Cap;
    my $terminal = Term::Cap->Tgetent({OSPEED => $termios->getospeed});

    # Force the clear characters to be output immediately.
    # Otherwise it might overlap with other output, like error messages.
    local $| = 1;

    print $terminal->Tputs('cl', 0);

    return 0;
}

sub runDialogExecutable
{
    my (@args) = @_;

    # Allow for 2 more file descriptors (on top of the normally allowed 0, 1,
    # 2) to survive the upcoming exec
    # See "SYSTEM_FD_MAX" in perldoc:perlvar
    $^F = 4;

    my $pipe = new IO::Pipe;
    my $pid;

    if ($pid = fork()) {
        # Parent
        $pipe->reader();

        my $output = <$pipe>;

        waitpid $pid, 0;
        my $result = ($? >> 8);
        $pipe->close();

        # dialog uses -1 as an exit code, Perl gets just the standard 8 bits
        # the rest of UNIX uses...
        if ($? == -1) {
            clearScreen();
            die "Failed to run dialog(1): $@";
        }
        elsif ($result == 255) {
            clearScreen();
            die "Canceled the dialog";
        }
        return $output || $result;
    }
    elsif (defined $pid) {
        # Child
        $pipe->writer();
        my $outputFd = $pipe->fileno();

        print "Using fd $outputFd";
        exec ('dialog', '--output-fd', $outputFd,
                        '--backtitle', 'kdesrc-build setup',
                        @args);
    }
    else {
        die "Unable to fork? $!";
    }
}

sub getUserInput
{
    my $prompt = shift;
    my $default = shift;

    my @args = qw/--inputbox 8 50/;
    splice @args, 1, 0, $prompt;
    push @args, $default if $default;

    return runDialogExecutable(@args);
}

sub getMenuOption
{
    my ($prompt, @opts) = @_;
    @opts = @{$opts[0]} if ref $opts[0] eq 'ARRAY';

    my @args = qw/--menu 20 70 18/;
    splice @args, 1, 0, $prompt;

    while(my ($k, $v) = splice (@opts, 0, 2)) {
        push @args, $k, $v;
    }

    return runDialogExecutable(@args);
}

sub showInfo
{
    my $message = shift;
    my @args = qw/--msgbox 20 62/;
    splice @args, 1, 0, $message;

    return runDialogExecutable(@args);
}

sub getYesNoAnswer
{
    my $prompt = shift;
    my @args = qw/--yesno 8 55/;
    splice @args, 1, 0, $prompt;

    return runDialogExecutable(@args) == 0;
}

sub getDirectory
{
    my $dir = shift;
    my @args = qw/--dselect 10 70/;
    splice @args, 1, 0, $dir;

    return runDialogExecutable(@args);
}

sub getListOptions
{
    my ($prompt, $opts, $enabled) = @_;
    die "\$opts not a hash ref" unless (ref $opts eq 'ARRAY');
    die "\$enabled not a hash ref" unless (ref $enabled eq 'HASH');

    my @args = qw/--checklist 20 70 18/;
    splice @args, 1, 0, $prompt;
    splice @args, 0, 0, '--output-separator', ',';

    while (my ($k, $v) = splice(@{$opts}, 0, 2)) {
        push (@args, $k, $v, (exists ${$enabled}{$k} ? 'on' : 'off'));
    }

    my $output = runDialogExecutable(@args);

    # Filter out empty results, remove quotes.
    my @items = split (/,/, $output);
    s/^"(.*)"$/$1/ foreach @items;
    @items = grep { length $_ } @items;
    return @items;
}

# The 'dialog(1)' program is required, verify it exists before going
# further.
# We use the --help option since it doesn't send weird terminal characters to the screen
# and it's supported on dialog and Debian's dialog replacement called whiptail.
system('dialog', '--help') == 0 or do {
    my $osError = "$!";

    say "Unable to run the dialog(1) program, it is required for this setup script.";
    if ($? == -1) {
        say "\tThe program wouldn't even run, due to error: $osError";
    }
    else {
        say "\tProgram ran, but exited with error: ", $? >> 8;
    }

    exit 1;
};

showInfo(<<EOF);
This program sets up a base kdesrc-build configuration to
use.

It can be modified as you wish later. Before the form is
presented, you will be asked if you would like an
explanation of the kdesrc-build file layout.  It is
recommended to read this if you are not already familiar
with building software.
EOF

if (getYesNoAnswer('See the tutorial?')) {
    showInfo(<<EOF);
kdesrc-build must download source code from the KDE
repositories.  This source code is then compiled, in the
"build directory". Once complete, this compiled code is
installed to its final location, the "install directory".

This program will only configure the install location, but
all directories are configurable.

The space requirements vary with the amount of software you choose
to build, and whether you keep the build directories to speed up
later builds.  You will probably need at least 20 GiB in total free
space unless you take steps to customize your install to use fewer
modules.
EOF
}

# If the user appears to be using a proxy, ask for it directly, otherwise
# prompt for one.
my $proxy = $ENV{http_proxy} // '';

my $installDir = getMenuOption('Where do you want to install the software?',
    [
        home => "$ENV{HOME}/kde/usr (default)",
        custom => "Custom location, chosen next screen",
    ]);

if ($installDir eq 'custom') {
    $installDir = getDirectory('/usr/local/kde');
}
else {
    $installDir = "~/kde/usr";
}

my $sourceDir = getMenuOption('Where do you want the source code to be saved?',
    [
        home => "$ENV{HOME}/kde/src (default)",
        custom => "Custom location, chosen next screen",
    ]);

if ($sourceDir eq 'custom') {
    $sourceDir = getDirectory('/usr/local/kde/src');
}
else {
    $sourceDir = "~/kde/src";
}

my $buildDir = getMenuOption('Where do you want temporary build files to be saved? (They might need lots of space)',
    [
        home => "$ENV{HOME}/kde/build (default)",
        custom => "Custom location, chosen next screen",
    ]);

if ($buildDir eq 'custom') {
    $buildDir = getDirectory('/usr/local/kde/build');
}
else {
    $buildDir = "~/kde/build";
}

my @chosenModules = getListOptions(
    "Which major module groups do you want to build?",
    [
        qt5 => 'Qt 5 - Base support libraries (required if distro version is old)',
        frameworks => 'KDE Frameworks 5 - Essential libraries/runtime (required)',
        workspace => 'KDE Plasma 5 Desktop and workspace',
        base => 'Assorted useful KF5-based applications',
        pim => 'Personal Information Management software',
    ],
    {
        frameworks => 1,
        workspace => 1,
        base => 1,
    },
);

my $numCpus = getUserInput(
    'How many CPU cores do you wish to use for building?', '4');

my $outputFileName = "$ENV{HOME}/.kdesrc-buildrc";
my $output; # Will be output filehandle.

while (-e $outputFileName) {
    (my $printableName = $outputFileName) =~ s/^$ENV{HOME}/~/;
    my $outputChoice = getMenuOption(
        "$printableName already exists, what do you want to do?",
        [
            backup => 'Make a backup, then overwrite with the new configuration',
            custom => 'Write the new configuration to a different file',
            cancel => 'Cancel setup',
        ],
    );

    if ($outputChoice eq 'cancel') {
        showInfo('Setup canceled');
        exit 0;
    }

    if ($outputChoice eq 'custom') {
        $outputFileName = getUserInput('Enter desired configuration file name.');
        $outputFileName =~ s/^~/$ENV{HOME}/;
    }

    if ($outputChoice eq 'backup') {

        copy($outputFileName, "$outputFileName~") or do {
            my $error = "$!";
            showInfo(<<EOF);
Failed to make backup of $outputFileName, due to error $error.
Configuration will be written to a temporary file instead.
EOF

            ($output, $outputFileName) = tempfile("kdesrc-buildrc-XXXX");
        };

        last;
    }
}

# Filehandle could already be opened as a tempfile.
if (!$output) {
    open ($output, '>', $outputFileName) or do {
        my $error = "$!";
        showInfo (<<EOF);
Unable to open output file $outputFileName for writing due to error $error.
EOF
        die "$!";
    }
}

print $output <<EOF;
# Autogenerated by kdesrc-build-setup. You may modify this file if desired.
global
EOF

# Only set qtdir if we're building it ourselves. If user uses their own custom
# Qt they should already be setting PATH and in that case we need do nothing
# anyways.
if (grep /^qt5$/, @chosenModules) {
    print $output <<EOF;

    # The path to your Qt installation (default is empty, assumes Qt provided
    # by system)
    qtdir ~/kde/qt5
EOF
}

print $output <<EOF;

    # Finds and includes *KDE*-based dependencies into the build.  This makes
    # it easier to ensure that you have all the modules needed, but the
    # dependencies are not very fine-grained so this can result in quite a few
    # modules being installed that you didn't need.
    include-dependencies true

    # Install directory for KDE software
    kdedir $installDir

    # Directory for downloaded source code
    source-dir $sourceDir

    # Directory to build KDE into before installing
    # relative to source-dir by default
    build-dir $buildDir

    # Use multiple cores for building. Other options to GNU make may also be
    # set.
    make-options -j$numCpus

    # kdesrc-build can install a sample .xsession file for "Custom"
    # (or "XSession") logins,
    install-session-driver false

    # or add a environment variable-setting script to
    # ~/.config/kde-env-master.sh
    install-environment-driver true

    # Stop the build process on the first failure
    stop-on-failure true

    # Use a flat folder structure under $sourceDir and $buildDir
    # rather than nested directories
    ignore-kde-structure true
EOF

if ($proxy) {
    print $output <<EOF;

    # Proxy to use for HTTP downloads.
    http-proxy $proxy

    # Prefer HTTPS instead of Git-native protocol for git modules that come
    # from 'kde-projects' repositories.
    #
    # Note that any git:// repositories you use will need to be
    # manually converted to https:// URLs if your network does not allow
    # git:// protcol.
    git-desired-protocol https
EOF
}

print $output <<EOF;
end global

EOF

# Assume we can refer to files present alongside kdesrc-build in the source
# directory
my $basedir = dirname(abs_path($0));
if (! -e "$basedir/kf5-frameworks-build-include") {
    # Check if it's installed to a share/ prefix
    $basedir = abs_path(dirname($0) . "/../share/kdesrc-build/");

    if (! -e "$basedir/kf5-frameworks-build-include") {
        close $output;
        showInfo("Unable to find kdesrc-build installation to build a configuration!");
        exit 1;
    }
}

if (grep /^qt5$/, @chosenModules) {
    print $output <<EOF;

# Refers to the qt5 file included as part of kdesrc-build. The file
# is simply read-in at this point as if you'd typed it in yourself.
include $basedir/qt5-build-include

# Support libraries that use Qt5
include $basedir/custom-qt5-libs-build-include

EOF
}

if (grep /^frameworks$/, @chosenModules) {
    print $output <<EOF;

# Refers to the kf5-frameworks file included as part of kdesrc-build. The file
# is simply read-in at this point as if you'd typed it in yourself.
include $basedir/kf5-frameworks-build-include

EOF
}

if (grep /^workspace$/, @chosenModules) {
    print $output <<EOF;

# Refers to the kf5-workspace file included as part of kdesrc-build. The file
# is simply read-in at this point as if you'd typed it in yourself.
include $basedir/kf5-workspace-build-include

EOF
}

if (grep /^base$/, @chosenModules) {
    print $output <<EOF;

# Refers to the kf5-applications file included as part of kdesrc-build. The file
# is simply read-in at this point as if you'd typed it in yourself.
include $basedir/kf5-applications-build-include

EOF
}

if (grep /^pim$/, @chosenModules) {
    print $output <<EOF;

# Refers to the kf5-kdepim file included as part of kdesrc-build. The file
# is simply read-in at this point as if you'd typed it in yourself.
include $basedir/kf5-kdepim-build-include

EOF
}

close($output);
$outputFileName =~ s/^$ENV{HOME}/~/;
showInfo("Generated configuration has been written to $outputFileName");

# Say same thing in text mode just in case.
system('clear');
say "Generated configuration has been written to $outputFileName";

if ($outputFileName ne '~/.kdesrc-buildrc') {
    say <<EOF;

Note that your configuration file in $outputFileName will
NOT BE USED unless you either:
1. Overwrite your ~/.kdesrc-buildrc with $outputFileName, or
2. Copy $outputFileName to be called 'kdesrc-buildrc' in some directory
   and ALWAYS run kdesrc-build from the directory, or
3. ALWAYS pass the "--rc-file $outputFileName" option to kdesrc-build when you
   run it.
EOF
}

exit 0;
