#!/usr/bin/perl
#
# vim: ts=4:noet
#
# sboconfig
# script to handle sbotools configuration
#
# authors: Jacob Pipkin <j@dawnrazor.net>
#          Luke Williams <xocel@iquidus.org>
#          Andreas Guldstrand <andreas.guldstrand@gmail.com>
# maintainer: K. Eugene Carlson <kvngncrlsn@gmail.com>
# license: MIT License

use 5.16.0;
use strict;
use warnings FATAL => 'all';
use SBO::Lib qw/ :config :colors slurp usage_error script_error lint_sbo_config $tempdir open_fh prompt wrapsay show_version /;
use File::Basename;
use Getopt::Long qw(:config no_ignore_case_always);
use File::Copy;
use File::Path qw(make_path);
use File::Temp qw(tempfile);;

my $self = basename($0);
my $label = $is_sbotest ? "sbotest config" : $self;

sub show_usage {
	if ($is_sbotest) {
		show_sbotest_usage();
		return 1;
	}
	print <<"EOF";
Usage: $label option argument ...
       $label

Options:
  -h|--help:
    this screen.
  -v|--version:
    version information.
  -l|--list:
    show current options.
  -n|--non-default:
    show current non-default options.
  --reset:
    restore the default configuration.

Configuration options (defaults shown):
  -B|--branch FALSE:
      GIT_BRANCH: git branch to use, or FALSE for the OS version default.
  -b|--build-ignore FALSE:
      BUILD_IGNORE: if TRUE, only attempt upgrades if the version differs.
  -C|--classic FALSE:
      CLASSIC: if TRUE, BUILD_IGNORE and RSYNC_DEFAULT; use 2.7 output.
  -c|--noclean FALSE:
      NOCLEAN: if TRUE, do not clean working directories after building.
  -D|--dialogrc FALSE:
      DIALOGRC: if a FILE, use it as dialogrc for sbotool.
  -d|--distclean FALSE:
      DISTCLEAN: if TRUE, clean source and package archives after building.
  -e|--etc-profile FALSE:
      ETC_PROFILE: if TRUE, source executable scripts in /etc/profile.d.
  -g|--gpg-verify FALSE:
      GPG_VERIFY: verify the repo with gnupg.
  -j|--jobs FALSE:
      JOBS: numeric -j setting to feed to make for multicore systems.
  -K|--color FALSE:
      COLOR: if TRUE, enable sbotools color output.
  -L|--log-dir FALSE:
      LOG_DIR: if an absolute path, save a log file here after each build.
  -P|--cpan-ignore FALSE:
      CPAN_IGNORE: install scripts even if they are installed from the CPAN.
  -p|--pkg-dir FALSE:
      PKG_DIR: set a directory to store packages in.
  -s|--sbo-home /usr/sbo:
      SBO_HOME: set the SBo directory.
  -O|--obsolete-check FALSE:
      OBSOLETE_CHECK: for -current, download obsolete list with sbocheck.
  -o|--local-overrides FALSE:
      LOCAL_OVERRIDES: a directory containing local overrides.
  -V|--slackware-version FALSE:
      SLACKWARE_VERSION: use the SBo repository for this version.
  -r|--repo FALSE:
      REPO: use a repository other than SBo.
  -R|--rsync FALSE:
      RSYNC_DEFAULT: default mirrors (other than for -current) are rsync.
  -S|--strict-upgrades FALSE:
      STRICT_UPGRADES: only upgrade when the version or build is higher.
  -w|--nowrap FALSE:
      NOWRAP: do not automatically wrap sbotools output.
  -X|--so-check FALSE:
      SO_CHECK: check for .so dependencies after sbocheck and sboupgrade.

Use $label without options for an interactive menu.
EOF
	return 1;
}

sub show_sbotest_usage {
	print <<"EOF";
Usage: $label option argument ...
       $label

Options:
  -h|--help:
    this screen.
  -v|--version:
    version information.
  -l|--list:
    show current options.
  -n|--non-default:
    show current non-default options.
  --reset:
    restore the default configuration.

Configuration options (defaults shown):
  -A|--sbo-archive /usr/sbotest/archive:
      SBO_ARCHIVE: the directory for package reuse.
  -B|--branch FALSE:
      GIT_BRANCH: git branch to use, or FALSE for the OS version default.
  -C|--classic FALSE:
      CLASSIC: if TRUE, BUILD_IGNORE and RSYNC_DEFAULT; use 2.7 output.
  -c|--noclean FALSE:
      NOCLEAN: if TRUE, do not clean working directories after building.
  -d|--distclean FALSE:
      DISTCLEAN: if TRUE, clean source and package archives after building.
  -e|--etc-profile TRUE:
      ETC_PROFILE: if FALSE, do not source executables in /etc/profile.d.
  -g|--gpg-verify FALSE:
      GPG_VERIFY: verify the repo with gnupg.
  -j|--jobs FALSE:
      JOBS: numeric -j setting to feed to make for multicore systems.
  -K|--color FALSE:
      COLOR: if TRUE, enable sbotools color output.
  -L|--log-dir /usr/sbotest/logs:
      LOG_DIR: if an absolute path, save a log file here after each build.
  -P|--cpan-ignore TRUE:
      CPAN_IGNORE: install scripts even if they are installed from the CPAN.
  -p|--pkg-dir /usr/sbotest/tests:
      PKG_DIR: set a directory to store packages in.
  -s|--sbo-home /usr/sbotest:
      SBO_HOME: set the SBo directory.
  -O|--obsolete-check FALSE:
      OBSOLETE_CHECK: for -current, download obsolete list with sbocheck.
  -o|--local-overrides FALSE:
      LOCAL_OVERRIDES: a directory containing local overrides.
  -V|--slackware-version FALSE:
      SLACKWARE_VERSION: use the SBo repository for this version.
  -r|--repo FALSE:
      REPO: use a repository other than SBo.
  -R|--rsync FALSE:
      RSYNC_DEFAULT: default mirrors (other than for -current) are rsync.
  -S|--strict-upgrades FALSE:
      STRICT_UPGRADES: with --archive-rebuild, only delete when version or
      build is lower.
  -w|--nowrap FALSE:
      NOWRAP: do not automatically wrap sbotest output.
  -X|--so-check FALSE:
      SO_CHECK: check for missing .so dependencies when running sbocheck.

Use $label without options for an interactive menu.
EOF
	return 1;
}

my $all_clear = 1 unless @ARGV;
my %options;

unless ($is_sbotest) {
	GetOptions(\%options, 'help|h', 'version|v', 'list|l', 'non-default|n', 'reset', 'classic|C=s', 'noclean|c=s',
		'distclean|d=s', 'jobs|j=s', 'pkg-dir|p=s', 'sbo-home|s=s',
		'local-overrides|o=s', 'slackware-version|V=s', 'repo|r=s',
		'build-ignore|b=s', 'branch|B=s', 'rsync|R=s', 'gpg-verify|g=s', 'strict-upgrades|S=s',
		'cpan-ignore|P=s', 'obsolete-check|O=s', 'etc-profile|e=s', 'log-dir|L=s',
		'nowrap|w=s', 'color|K=s', 'so-check|X=s', 'dialogrc|D=s');
} else {
	GetOptions(\%options, 'help|h', 'version|v', 'list|l', 'non-default|n', 'reset', 'classic|C=s', 'noclean|c=s',
		'distclean|d=s', 'jobs|j=s', 'pkg-dir|p=s', 'sbo-home|s=s',
		'local-overrides|o=s', 'slackware-version|V=s', 'repo|r=s',
		'build-ignore|b=s', 'branch|B=s', 'rsync|R=s', 'gpg-verify|g=s', 'strict-upgrades|S=s',
		'cpan-ignore|P=s', 'obsolete-check|O=s', 'etc-profile|e=s', 'log-dir|L=s',
		'nowrap|w=s', 'color|K=s', 'so-check|X=s', 'sbo-archive|A=s', 'dialogrc|D=s');
}

$options{list} = 1 if exists $options{'non-default'};
if ($options{help}) {
	show_usage();
	wrapsay "\nNon-root users can call $label with -l, -n, -h and -v." unless $< == 0;
	exit 0;
}
if ($options{version}) { show_version(); exit 0 }
unless ($< == 0 or $options{list}) {
	show_usage();
	usage_error "\nNon-root users can call $label with -l, -n, -h and -v.";
}

my %valid_confs = (
	classic             => 'CLASSIC',
	noclean             => 'NOCLEAN',
	distclean           => 'DISTCLEAN',
	jobs                => 'JOBS',
	'pkg-dir'           => 'PKG_DIR',
	'sbo-home'          => 'SBO_HOME',
	'local-overrides'   => 'LOCAL_OVERRIDES',
	'slackware-version' => 'SLACKWARE_VERSION',
	'repo'              => 'REPO',
	rsync               => 'RSYNC_DEFAULT',
	'branch'            => 'GIT_BRANCH',
	'build-ignore'      => 'BUILD_IGNORE',
	'gpg-verify'        => 'GPG_VERIFY',
	'strict-upgrades'   => 'STRICT_UPGRADES',
	'cpan-ignore'       => 'CPAN_IGNORE',
	'obsolete-check'    => 'OBSOLETE_CHECK',
	'etc-profile'       => 'ETC_PROFILE',
	'log-dir'           => 'LOG_DIR',
	color               => 'COLOR',
	nowrap              => 'NOWRAP',
	'so-check'          => 'SO_CHECK',
	'dialogrc'          => 'DIALOGRC',
);
$valid_confs{"sbo-archive"} = 'SBO_ARCHIVE' if $is_sbotest;

my %params = (
	CLASSIC           => 'C|--classic',
	NOCLEAN           => 'c|--noclean',
	DISTCLEAN         => 'd|--distclean',
	GPG_VERIFY        => 'g|--gpg-verify',
	JOBS              => 'j|--jobs',
	PKG_DIR           => 'p|--pkg-dir',
	SBO_HOME          => 's|--sbo-home',
	LOCAL_OVERRIDES   => 'o|--local-overrides',
	SLACKWARE_VERSION => 'V|--slackware-version',
	REPO              => 'r|--repo',
	RSYNC_DEFAULT     => 'R|--rsync',
	GIT_BRANCH        => 'B|--branch',
	BUILD_IGNORE      => 'b|--build-ignore',
	STRICT_UPGRADES   => 'S|--strict-upgrades',
	CPAN_IGNORE       => 'P|--cpan-ignore',
	OBSOLETE_CHECK    => 'O|--obsolete-check',
	ETC_PROFILE       => 'e|--etc-profile',
	LOG_DIR           => 'L|--log-dir',
	COLOR             => 'K|--color',
	NOWRAP            => 'w|--nowrap',
	SO_CHECK          => 'X|--so-check',
	DIALOGRC          => 'D|--dialogrc',
);
$params{SBO_ARCHIVE} = 'A|--sbo-archive' if $is_sbotest;

if (exists $options{list}) {
	my @keys = sort {$a cmp $b} keys %config;
	if (exists $options{'non-default'} and not $is_sbotest) {
		for my $item (@keys) {
			next if $config{$item} eq 'FALSE';
			next if $item eq 'SBO_HOME' and $config{$item} eq '/usr/sbo';
			say "$label -$params{$item}:\n    $item=$config{$item}";
		}
	} elsif (exists $options{'non-default'} and $is_sbotest) {
		for my $item (@keys) {
			if ($item eq 'ETC_PROFILE' or $item eq 'CPAN_IGNORE') { next if $config{$item} eq 'TRUE'; }
			if ($item eq 'SBO_HOME') { next if $config{$item} eq 'FALSE' or $config{$item} eq '/usr/sbotest'; }
			if ($item eq 'SBO_ARCHIVE') { next if $config{$item} eq 'FALSE' or $config{$item} eq "$config{'SBO_HOME'}/archive"; }
			if ($item eq 'PKG_DIR') { next if $config{$item} eq 'FALSE' or $config{$item} eq "$config{'SBO_HOME'}/tests"; }
			if ($item eq 'LOG_DIR') { next if $config{$item} eq 'FALSE' or $config{$item} eq "$config{'SBO_HOME'}/logs"; }
			next if $item ne 'ETC_PROFILE' and $item ne 'CPAN_IGNORE' and $config{$item} eq 'FALSE';
			say "$label -$params{$item}:\n    $item=$config{$item}";
		}
	} else {
		say "$label -$params{$_}:\n    $_=$config{$_}" for @keys;
	}
	wrapsay "\nWarning: Local overrides directory $config{LOCAL_OVERRIDES} does not exist." if $config{LOCAL_OVERRIDES} ne "FALSE" and not -d $config{LOCAL_OVERRIDES};
	exit 0;
}

if (exists $options{reset}) {
	if (prompt($color_warn, "Reset all options to the default setting?", default => 'no')) {
		say "Restoring default configuration...";
	} else {
		say "Exiting without changes.";
		exit 0;
	}
}

# setup what's being changed, sanity check.
my %changes;
for my $key (keys %valid_confs) {
	my $value = $valid_confs{$key};
	if (exists $options{reset}) {
		$changes{$value} = "FALSE";
	} else {
		$changes{$value} = $options{$key} if exists $options{$key};
	}
}
$changes{'SBO_HOME'} = '/usr/sbo' if exists $options{reset};
if (exists $options{reset} and $is_sbotest) {
	$changes{'SBO_HOME'} =  '/usr/sbotest';
	$changes{'PKG_DIR'} = '/usr/sbotest/tests';
	$changes{'LOG_DIR'} = '/usr/sbotest/logs';
	$changes{'SBO_ARCHIVE'} = '/usr/sbotest/archive';
	$changes{'ETC_PROFILE'} = 'TRUE';
	$changes{'CPAN_IGNORE'} = 'TRUE';
}

if ($all_clear) {
	interactive_menu();
} elsif (not %options) {
	show_usage();
	exit 1;
}

lint_sbo_config($self, %changes);

my $change_requested;

# subroutine for prompting the user; takes configuration name,
# description, valid setting description and validation type.
sub config_prompt {
	script_error("config_prompt requires at least four arguments.") unless @_ >= 4;
	my ($config_name, $description, $validity_description, $validator) = @_;
	my $user_input;
	wrapsay_color $color_notice, "\n$config_name", 1;
	wrapsay "$description\n\n$validity_description";
	chomp($user_input = prompt($color_notice, "\tCurrent value is $config{$config_name}: "));
	return 0 unless $user_input;
	if ($validator) {
		if (validate_choice($config_name, $user_input, $validator)) {
			$change_requested = 1;
		} else {
			config_prompt($config_name, $description, $validity_description, $validator);
		}
	} else {
		$change_requested = 1;
		$changes{$config_name} = $user_input;
	}
}

# subroutine for validating the user's choice
sub validate_choice {
	script_error("validate choice requires three options.") unless @_ == 3;
	my ($config_name, $requested_change, $validator) = @_;
	$changes{$config_name} = $requested_change;
	my $failed;
	if ($validator eq "TF") {
		unless ($changes{$config_name} =~ /^(TRUE|FALSE)$/) {
			say "\nUse TRUE or FALSE for $config_name.";
			$failed = 1;
		}
	} elsif ($validator eq "PATH") {
		unless ($changes{$config_name} =~ qr#^(/|FALSE$)#) {
			say "\nUse an absolute path or FALSE for $config_name.";
			$failed = 1;
		}
	} elsif ($validator eq "NUM") {
		unless ($changes{$config_name} =~ /^(\d+|FALSE)$/) {
			say "\nUse a number or FALSE for $config_name.";
			$failed = 1;
		}
	} elsif ($validator eq "SWVER") {
		unless ($changes{$config_name} =~ m/^(\d+\.\d+(|\+)|FALSE|current)$/) {
			wrapsay "\nUse #.#, #.#+ or current for $config_name.";
			$failed = 1;
		}
	}
	if ($failed) {
		undef $changes{$config_name};
		return 0;
	}
	return 1;
}

# use an interactive settings menu if sboconfig is passed without
# options; might be educational for new users
sub interactive_menu {
	unless (prompt($color_default, "$label\n\nThis is the $label interactive menu. Use prompts to set configuration values?", default => 'yes')) {
		say "Exiting without changes.";
		exit 0;
	}
	wrapsay "All settings are case-sensitive. Enter an empty value to skip any setting. You will have a chance to confirm your settings at the end.";

	# here, give the following values:
	#
	# name of setting
	# description
	# description of a valid setting
	# validator, being "TF", "NUM", "PATH", "SWVER" or 0 for no validation
	CLASSIC: config_prompt("CLASSIC", "If TRUE, enable BUILD_IGNORE and RSYNC_DEFAULT; disable build increment and out-of-tree output for sbocheck; disable displaying saved build options. This is a more traditional look-and-feel.", "TRUE or FALSE.", "TF");

	DISTCLEAN: config_prompt("DISTCLEAN", "If TRUE, remove the source code and compiled package after building.", "TRUE or FALSE.", "TF");

	JOBS: config_prompt("JOBS", "If a number, use with -j in MAKEOPTS.", "A number or FALSE.", "NUM");

	NOCLEAN: config_prompt("NOCLEAN", "If TRUE, do not clean working directories after building.", "TRUE or FALSE.", "TF");

	GIT_BRANCH: config_prompt("GIT_BRANCH", "If set to a branch name, use a custom git branch for the SBo repository. Has no effect on rsync mirrors.", "Branch name or FALSE.", 0);

	GPG_VERIFY: config_prompt("GPG_VERIFY", "If TRUE, perform gpg verification.", "TRUE or FALSE.", "TF");

	# CLASSIC turns this on automatically at runtime; not applicable to sbotest
	BUILD_IGNORE: if ((not defined $changes{CLASSIC} and $config{CLASSIC} ne 'TRUE') and not $is_sbotest) {
		config_prompt("BUILD_IGNORE", "If TRUE, do upgrades only if the version number differs.", "TRUE or FALSE.", "TF");
	}

	CPAN_IGNORE: config_prompt("CPAN_IGNORE", "If TRUE, install scripts even if they are installed from the CPAN.", "TRUE or FALSE.", "TF");

	NOWRAP: config_prompt("NOWRAP", "If TRUE, do not wrap sbotools output.", "TRUE or FALSE.", "TF");

	SO_CHECK: config_prompt("SO_CHECK", "If TRUE, sbocheck and sboupgrade look for missing shared objects among SBo packages.", "TRUE or FALSE.", "TF");

	DIALOGRC: unless ($is_sbotest) {
		config_prompt("DIALOGRC", "If a FILE, use it as dialogrc when running sbotool.", "An absolute path or FALSE.", "PATH");
	}

	OBSOLETE_CHECK: config_prompt("OBSOLETE_CHECK", "If TRUE, sbocheck updates the obsolete script list from the sbotools home page.", "TRUE or FALSE.", "TF");

	PKG_DIR: config_prompt("PKG_DIR", "If set to an absolute path, store newly-built packages there regardless of DISTCLEAN.", "An absolute path or FALSE.", "PATH");

	ETC_PROFILE: config_prompt("ETC_PROFILE", "If TRUE, source executable *.sh scripts in /etc/profile.d before building.", "TRUE or FALSE.", "TF");

	SBO_HOME: config_prompt("SBO_HOME", "If set to an absolute path, this is where the SlackBuilds.org tree will live.", "An absolute path or FALSE.", "PATH");

	# sbotest only
	SBO_ARCHIVE: config_prompt("SBO_ARCHIVE", "If set to an absolute path, this is where archived packages will live.", "An absolute path or FALSE.", "PATH") if $is_sbotest;

	LOG_DIR: config_prompt("LOG_DIR", "If set to an absolute path, save a log file for each build here.", "An absolute path or FALSE.", "PATH");

	LOCAL_OVERRIDES: config_prompt("LOCAL_OVERRIDES", "If set to an absolute path, any directory name in the top level under that path matching a SlackBuild name will be used in preference to the main repository. Personalized builds can be stored here.", "An absolute path or FALSE.", "PATH");

	STRICT_UPGRADES: config_prompt("STRICT_UPGRADES", "If TRUE, only perform upgrades when the incoming version or build number is higher. This has no effect on scripts in the local overrides directory.", "TRUE or FALSE.", "TF");

	SLACKWARE_VERSION: config_prompt("SLACKWARE_VERSION", "If set to a version specification (e.g. 14.2, 15.0+ or current), force the use of the SBo repository for that version.", "Use #.#, #.#+, current or FALSE.", "SWVER");

	REPO: config_prompt("REPO", "If set to a URL, use this as the upstream SBo repository. Git and rsync repositories only.", "A URL or FALSE.", 0);

	# CLASSIC turns this off automatically at runtime; not applicable to sbotest
	COLOR: if ((not defined $changes{CLASSIC} and $config{CLASSIC} ne 'TRUE') and not $is_sbotest) {
		config_prompt("COLOR", "If TRUE, turn on sbotools color output.", "TRUE or FALSE.", "TF");
	}

	# CLASSIC turns this on automatically at runtime
	RSYNC_DEFAULT: if (not defined $changes{CLASSIC} and $config{CLASSIC} ne 'TRUE') {
		config_prompt("RSYNC_DEFAULT", "If TRUE, the default mirror will be rsync except for Slackware -current. Please note that REPO overrides this setting.", "TRUE or FALSE.", "TF");
	}

	if ($change_requested) {
		wrapsay "\nDone. The following settings have been specified:", 1;
		for my $item (keys %valid_confs) {
			my $title = $valid_confs{$item};
			my $spacer = "\t\t";
			$spacer = "\t\t\t" if $title eq "JOBS" or $title eq "REPO";
			$spacer = "\t" if $title eq "SLACKWARE_VERSION" or $title eq "LOCAL_OVERRIDES" or $title eq "STRICT_UPGRADES" or $title eq "OBSOLETE_CHECK" or $title eq "SBO_ARCHIVE";
			say "\t$title:$spacer$changes{$title}" if defined $changes{$title};
		}

		unless (prompt($color_lesser, "\nWrite these settings to $conf_file?", default => 'yes')) {
			if (prompt($color_notice, "Start this menu over?", default => 'no')) {
				undef %changes;
				interactive_menu();
			} else {
				say "Exiting without changes.";
				exit 0;
			}
		}
	} else {
		say "\nNo settings were specified. Exiting.";
		exit 0;
	}
}

sub config_write {
	script_error('config_write requires at least two arguments.') unless @_ >= 2;

	if (! -d $conf_dir) {
		mkdir $conf_dir or usage_error("Unable to create $conf_dir. Exiting.");
	}

	my $conf = slurp($conf_file) || '';
	_fixup_conf($conf);

	while (@_ >= 2) {
		my $key = shift;
		my $val = shift;
		next if $key eq 'BUILD_IGNORE' and $is_sbotest;
		next if $key eq 'COLOR' and $is_sbotest;
		say "Setting $key to $val..." unless exists $options{reset};

		# Comment default values when written in
		my $comment = '';
		unless ($is_sbotest) {
			$comment = '#' if $val eq 'FALSE';
			$comment = '#' if $key eq 'SBO_HOME' and $val eq '/usr/sbo';
		} else {
			$comment = '#' if $key ne 'ETC_PROFILE' and $key ne 'CPAN_IGNORE' and $val eq 'FALSE';
			$comment = '#' if ($key eq 'ETC_PROFILE' or $key eq 'CPAN_IGNORE') and $val eq 'TRUE';
			$comment = '#' if $key eq 'SBO_HOME' and $val eq '/usr/sbotest';
			$comment = '#' if $key eq 'PKG_DIR' and $val eq '/usr/sbotest/tests';
			$comment = '#' if $key eq 'LOG_DIR' and $val eq '/usr/sbotest/logs';
			$comment = '#' if $key eq 'SBO_ARCHIVE' and $val eq '/usr/sbotest/archive';
		}

		if ($conf =~ /^#(\s*)\Q$key\E=/m) {
			$conf =~ s/^#(\s*)\Q$key\E=.*$/$comment$key=$val/m;
		} elsif ($conf =~ /^\Q$key\E=/m) {
			$conf =~ s/^\Q$key\E=.*$/$comment$key=$val/m;
		} else {
			$conf .= "$comment$key=$val\n";
		}
	}

	_fixup_conf($conf);

	my ($conffh, $exit) = open_fh($conf_file, '>');
	error_code("Failed to open $conf_file; exiting.", $exit) if $exit;
	print {$conffh} $conf;
}

# make sure there are no duplicate keys in the config
sub _fixup_conf {
	my @lines = split /\n/, $_[0];
	my @fixed;
	my %keys;
	foreach my $line (@lines) {
		# if it's a comment or blank line, just pass it through
		if ($line =~ /^(#|\s*$)/) { push @fixed, $line; next; }

		my ($key, $val) = split /=/, $line;
		next if exists $keys{$key};
		$keys{$key}++;
		push @fixed, $line;
	}

	$_[0] = join "\n", @fixed, ''; # make sure we end with a newline if there are any lines
}

if (%changes) {
	config_write(%changes);
}

END { say ""; }
