#!/usr/bin/perl -w


# Copyright (c) 1998 Doug Alcorn <alcornd@earthlink.net>

# 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., 675 Mass Ave, Cambridge, MA 02139, USA.

###################
# Purpose
#
# The purpose of this script is to take a minimal set of data and apply
# it to the ~/G/L/A directory tree to implement a "theme" for AfterStep.
# This script works on the assumption of version 1.5 or greater.

###################
# Scope
#
# Here are the individual components that I think make up a theme.
# Obviously, the individual look file in its entirety.  Also the Wharf
# TextureType, Pixmap, TextureColor, and BgColor.  The Pager Font,
# SmallFont, Fore, Back, Hilight, DesktopImage, Image, Align,
# BalloonFore, BalloonBack, BalloonFont, BalloonBorderWidth, and
# BalloonBorderColor. Finally, the WinListFont, WinListFore,
# WinListBack, and WinListJustify.  As an added bonus, a scheme will
# be included to allow icons for both Wharf and the database to be
# replaced.

###################
# Assumptions
#
# * Uniqueue themes for each desk will not be supported at this time.
# * Modules that support multiple instances must be formed like this:
#   <new_prefix>Pager versus Wharf<bad_suffix>.  Examples might be
#   MyPager, GoodPager, MyWharf not Wharf2, WharfBad, PagerBad


################### 
# Strategy 
# 
# A new themes directory will be placed in the ~/G/L/A/desktop
# directory.  Each new theme will have its own subdirectory under
# that. Each theme's subdirectory will duplicate the ~/G/L/A/desktop
# subdirectores plus put its own "patches" to the module configuration
# files there.  For example, let's say I had a theme called 'foo'. I
# would then have a directory ~/G/L/A/desktop/themes/foo.  Under that
# I would have the 16bpp, 8bpp, common subdirs and possibly 24bpp and
# other bit depths.  Also in the ~/G/L/A/desktop/themes/foo directory
# would be wharf.foo, pager.foo, and winlist.foo.  OK, that sets up
# how the theme data is stored.
#
# Now on the startmenu, in the Decorations submenu would be a new menu
# called "Themes".  When the startmenu is built, it will scan the
# ~/G/L/A/desktop/themes directory to determine what themes are
# "installed".  When the user then selects a theme from the startmenu,
# this theme installer script will then be executed to modify the
# appropriate files in the non-configurable directory.  
#

###################
# Defines
my $AFTERSTEP_DIR="$ENV{'HOME'}/GNUstep/Library/AfterStep";
my $NONCONFIG_DIR="${AFTERSTEP_DIR}/non-configurable";
my $THEME_DIR="${AFTERSTEP_DIR}/desktop/themes";
my $BIT_DEPTH="8bpp";
my $THEME_NAME="DEFAULT";
my %FILE_NAMES = ( 'wharf' => "${AFTERSTEP_DIR}/wharf",
		  'pager' => "${AFTERSTEP_DIR}/pager",
		  'winlist' => "${AFTERSTEP_DIR}/winlist",
		  'background' => "${NONCONFIG_DIR}/0_background",
		);
my %IMAGE_OPTIONS = ( 
		     "Pixmap" => "TRUE",
		     "DesktopImage" => "TRUE",
		     "Image" => "TRUE",
		     "Pixmap" => "TRUE",
		     "TitleButton" => "TRUE",
		     "ButtonPixmap" => "TRUE",
		     "MenuPinOn" => "TRUE",
		     "MenuPinOff" => "TRUE",
		     "BackPixmap" => "TRUE",
		    );
###################
# Flags
my %MOD_FLAGS = ( 'wharf' => "TRUE",
		  'pager' => "TRUE",
		  'winlist' => "TRUE",
		  'background' => "TRUE",
		);
		 
##################
# Command Line Syntax Checking
sub print_usage {
  warn "Usage: $0 [--theme <theme_name>] [--no_wharf] [--no_pager] [--no_winlist] [--no_background]\n";
}

if ((scalar(@ARGV) > 7)) {
  warn "Incorrect number of arguments, $ARGV\n";
  print_usage();
  die;
}

while (scalar(@ARGV) >= 1) {
  my $argument = shift;
  if (($argument eq "--theme") and (scalar(@ARGV) >=1)) {
    $THEME_NAME = shift;
    if ($THEME_NAME =~ /^--no_((wharf)|(pager)|(winlist)|(background))/) {
      print_usage();
      die;
    }
  } elsif ($argument eq "--no_wharf") {
    delete $MOD_FLAGS{'wharf'};
  } elsif ($argument eq "--no_pager") {
    delete $MOD_FLAGS{'pager'};
  } elsif ($argument eq "--no_winlist") {
    delete $MOD_FLAGS{'winlist'};
  } elsif ($argument eq "--no_background") {
    delete $MOD_FLAGS{'background'};
  } else {
    print_usage();
    die;
  }
}

if ( ! -d "${THEME_DIR}/${THEME_NAME}" ) {
  die "Could not find theme ${THEME_NAME} in the ${THEME_DIR} directory.\n";
}
$THEME_DIR = "${THEME_DIR}/${THEME_NAME}";

###################
# Configuration Checking

if ( ! -d $AFTERSTEP_DIR ) {
  warn "Could not find the $AFTERSTEP_DIR directory.\n";
  die "Are you sure you are running AfterStep verion >= 1.5?\n";
}

if ( ! -d $NONCONFIG_DIR ) {
  warn "Could not find $NONCONFIG_DIR directory.\n";
  die "Are you sure you are running AfterStep verion >=1.5?\n";
}

if ( ! -d $THEME_DIR ) {
  die "Could not find $THEME_DIR directory.\n";
}

if ( ! -f "${THEME_DIR}/wharf.${THEME_NAME}" ) {
  warn "Cannot find the  config file, ${THEME_DIR}/wharf.${THEME_NAME}\n";
  warn "Will not apply theme to Wharf.\n";
  delete $MOD_FLAGS{'wharf'};
} elsif ( ! -f $FILE_NAMES{'wharf'} ) {
  warn "Cannot find the config file, $FILE_NAMES{'wharf'}\n";
  warn "Will not apply theme to Wharf.\n";
  delete $MOD_FLAGS{'wharf'};
}

if ( ! -f "${THEME_DIR}/pager.${THEME_NAME}" ) {
  warn "Cannot find the config file, ${THEME_DIR}/pager.${THEME_NAME}\n";
  warn "Will not apply theme to Pager.\n";
  delete $MOD_FLAGS{'pager'};
} elsif ( ! -f $FILE_NAMES{'pager'} ) {
  warn "Cannot find the Pager config file, $FILE_NAMES{'pager'}\n";
  warn "Will not apply theme to Pager.\n";
  delete $MOD_FLAGS{'pager'};
}

if ( ! -f "${THEME_DIR}/winlist.${THEME_NAME}" ) {
  warn "Cannot find the config file, ${THEME_DIR}/winlist.${THEME_NAME}\n";
  warn "Will not apply theme to WinList\n";
  delete $MOD_FLAGS{'winlist'};
} elsif ( ! -f $FILE_NAMES{'winlist'} ) {
  warn "Cannot find the WinList config file, $FILE_NAMES{'winlist'}\n";
  warn "Will not apply theme to WinList.\n";
  delete $MOD_FLAGS{'winlist'};
}

if ( ! -f "${THEME_DIR}/background.${THEME_NAME}") {
  warn "Cannot find the image file, ${THEME_DIR}/background.${THEME_NAME}\n";
  warn "Will not apply theme to background image\n";
  delete $MOD_FLAGS{'background'};
}


# Get bit depth via xdpyinfo
open (PROC, "xdpyinfo|") or
  die "Cannot start 'xdpyinfo' process, $!\n";
while (<PROC>) {
  # looking for a line that has the key word "depths" on it, as in:
  # screen #0:
  #  dimensions:    1024x768 pixels (347x260 millimeters)
  #  resolution:    75x75 dots per inch
  #  depths (1):    16
  next unless /depths/;
  # strip out all the stuff except the actual bit depth
  chomp;			# and the pesky newlines

  s {
     \s*			# Match any beginning spaces
     depths			# Catch the keyword "depths"
     \s+			# follwed by some more spaces
     \(
     (\d+)			# some number of digets 
                                # (remember them for later)
     \)				# the digets are enclosed in parens
     :\s*			# follwed by a colon and some more spaces
     }[]x;			# replace all that with nothing (erase it)
  # The number in parenthases is the length of a list of bit depths
  # We will make an array of that list and read the last one as the actual
  # bit depth
  my @depths = split /,\s/, $_, $1;
  $BIT_DEPTH = $depths[scalar(@depths) -1];
  last;
}

# Check to make sure we got the bit depth from xdpyinfo
unless ($BIT_DEPTH) {
  die "Couldn't figure out bit depth from xdpyinfo\n";
}

# Now that we know the bit depth, we know which look and feel file to
# modify in the non-config directory
$FILE_NAMES{'look'}="${NONCONFIG_DIR}/0_look.${BIT_DEPTH}bpp";
$FILE_NAMES{'base'}="${AFTERSTEP_DIR}/base.${BIT_DEPTH}bpp";

if ( ! -f $FILE_NAMES{'look'} ) {  
  warn "Cannot find the non-configurable look file for your bit depth, $FILE_NAMES{'look'}\n";
  die "Are you sure you are using AfterStep >= 1.5?\n";
}

if ( ! -f $FILE_NAMES{'base'} ) {
  warn "Cannot find the base file for your depth, $FILE_NAMES{'base'}\n";
  die "Are you sure you are running AfterStep >=1.5?\n";
}

# OK, everything is kocher; let's do the easy stuff first...
open LOOK_SOURCE, "${THEME_DIR}/look.${THEME_NAME}" or
  die "Cannot read from theme look file ${THEME_DIR}/look.${THEME_NAME}, $!\n";
@look_source = <LOOK_SOURCE>;
close LOOK_SOURCE;

# Go through and fix the paths of all the images
for ($line = 0; $line < scalar(@look_source); $line++) {
  # Skip comment lines (there shouldn't really be any)
  next if ($look_source[$line] =~/^\#/);
  # split and save any leading spaces...
  my $spaces;
  $look_source[$line] =~ s/^(\s+)//;
  if (defined $1) {
    $spaces = $1;
  } else {
    $spaces = "";
  }
  if ($look_source[$line] eq "") {
    s/^/$spaces/;
    next;
  }
  # split the line on the first white space
  my ($option, $parameter) = split /\s+/, $look_source[$line], 2;
  # What might be nice here is to check to make sure that only
  # "approved" options are in the list...
  if (exists $IMAGE_OPTIONS{$option}) {
    # I need to make sure the path specified on images is correct
    if ($option eq "TitleButton") {
	# TitleButton works different from the other image options It
	# has multiple arguments in the $parameter, and the first of
	# which is not an image.
	my ($button, $unpressed, $pressed) = split /\s+/, $parameter, 3;
	$unpressed = &fix_path($option, $unpressed);
	$pressed = &fix_path($option, $pressed);
	$parameter = $button . " " . $unpressed . " " . $pressed;
	$look_source[$line] = $option . " " . $parameter;
      } elsif ($option eq "BackPixmap") {
	my ($type, $image) = split /\s+/, $parameter, 2;
	$image = &fix_path($option, $image);
	$look_source[$line] = $option . " " . $type . " " . $image;
      } else {
	# This means we have a standard image option, which just has
	# the image filename as a parameter
	$parameter = &fix_path($option, $parameter);
	$look_source[$line] = $option . " " . $parameter;
      }
  }
  # put the spaces back at the beginning of the line
  $look_source[$line] =~ s/^/$spaces/;
}

# copy the theme look and feel file to "standard" places
open LOOK_TARGET, ">${AFTERSTEP_DIR}/looks/look.${THEME_NAME}" or
  die "Cannot write to ${AFTERSTEP_DIR}/looks/look.${THEME_NAME}, $!\n";
print LOOK_TARGET @look_source;
close LOOK_TARGET;

# while we're here, let's put a copy in the non-config dir
open LOOK_TARGET, ">$FILE_NAMES{'look'}" or
  die "Cannot write to $FILE_NAMES{'look'}, $!\n";
print LOOK_TARGET @look_source;
close LOOK_TARGET;

# Let's copy the background over to the non-config dir
if (exists $MOD_FLAGS{'background'}) {
  open BACKGROUND_TARGET, ">$FILE_NAMES{'background'}" or
    die "Cannot write to $FILE_NAMES{'background'}, $!\n";
  open BACKGROUND_SOURCE, "${THEME_DIR}/background.${THEME_NAME}" or
    die "Cannot read ${THEME_DIR}/background.${THEME_NAME}, $!\n";
  print BACKGROUND_TARGET <BACKGROUND_SOURCE>;
  close BACKGROUND_TARGET;
  close BACKGROUND_SOURCE;
  delete $MOD_FLAGS{'background'};
}

# OK, in the wharf, pager, and winlist file only the options that need
# to be replaced from the config files.  Lets read the options into a
# hash.  Some assumptions here, any multiple Wharfs, or Pagers will
# begin with *<new_name>Pager rather than *Pager<new_name>.  This
# could probably be done with a subroutine.

foreach $module (keys %MOD_FLAGS) {
  &edit_config($module);
}

# The thought on the base.bpp file is this: if we prefix the PixmaPath
# with our theme dir, and icons in that path will be used instead of
# the normal ones.  This will allow the themeifying of the window
# icons and wharf icons without actually having to modify the wharf
# icon names or database icon names.  The only problem might be with having
# a line that is too long.

rename $FILE_NAMES{'base'}, "$FILE_NAMES{'base'}.temp" or
  die "Cannot rename $FILE_NAMES{'base'}, $!\n";
open BASE_INPUT, "$FILE_NAMES{'base'}.temp" or
  die "Cannot read $FILE_NAMES{'base'}.temp, $!\n";
open BASE_FILE, ">$FILE_NAMES{'base'}" or
  die "Cannot write to $FILE_NAMES{'base'}, $!\n";

while(<BASE_INPUT>) {
  # Let's arbitrarily hammer any directories in the PixmapPath that
  # are subdirs of ~g/l/a/desktop/themes...  If anyone complains, I
  # don't know how to fix it.  The problem is that if a bunch of
  # themes get applied in a row, the PixmapPath will become too long.
  #
  # Note that if the $HOME directory is replaced with the '~', I won't
  # hammer it with this substitute...
  s{
    $AFTERSTEP_DIR/desktop/themes/
    [^:]*			# Match all the next stuff up to a
				# colon 
   }[]xg;
  s{
    ^				# Start at the beginning of the line
    (				# we will remember the entire thing we match
    \s*				# Match any leading spaces
    PixmapPath			# Look for the keyword "PixmapPath"
    \s+				# followed by some amount of spaces.
    )
   } [${1}${THEME_DIR}:]x;	# Replace this with what we just
                                # matched followed by the directory
				# with all the theme images in it.
				# This effectively "inserts" the
				# THEME_DIR at the beginning of the
				# PixmapPath
   s/\:{2,}/\:/;		# This should eliminate multiple
                                # colons in a row 
  print BASE_FILE;
}
close BASE_FILE;
close BASE_INPUT;

sub edit_config {

  # This little routing is a way to genericly edit a module's config
  # file.  It does this by evaluating the argument passed (the lower
  # case version of the module name, eg 'wharf') in varying contexts
  # to get the global variables needed for that module.
  
  my $module_name = shift;
  my $proper_name = substr $module_name, 0, 1;
  $proper_name =~ tr /a-z/A-Z/;
  $proper_name .= substr $module_name, 1;
  my %module_options;
  open MODULE_THEME, "${THEME_DIR}/${module_name}.${THEME_NAME}" or
    die "Cannot read from the ${THEME_DIR}/${module_name}.${THEME_NAME}, $!\n";

  while (<MODULE_THEME>) {
    # Skip comment lines (there shouldn't really be any)
    next if /^\#/;
    # split the line on the first white space
    my ($option, $parameter) = split /\s+/, $_, 2;
    # What might be nice here is to check to make sure that only
    # "approved" options are in the list...
    if (exists $IMAGE_OPTIONS{$option}) {
      if ($option eq "DesktopImage" or $option eq "Image") {
	# Because of the multiple desks and images, we need to handle
	# the PagerImage and PagerDesktopImage differently.
	# Specifically, I will store the desk number for later
	# reference in the target pager config.
	my ($desk, $image) = split /\s+/, $parameter, 2;
	$image = &fix_path($option, $image);
	unless ($module_options{$option}) {
	  # if this is the first option found that ends in "Image"
	  # initialize the hash to point to an anonymous hash
	  $module_options{$option} = {};
	}
	$module_options{$option}{$desk} = $image;
      } else {
	# This means we have a "standard" image option and need to
	# strip any possible specifying path off
	$parameter = &fix_path($option, $parameter);
	$module_options{$option} = $parameter;
      }
    } else {
      $module_options{$option} = $parameter;
    }
  }
  close MODULE_THEME;

  rename $FILE_NAMES{$module_name}, "$FILE_NAMES{$module_name}.temp" or
    die "Cannot make temp file $FILE_NAMES{$module_name}.temp, $!\n";
  open MODULE_INPUT, "$FILE_NAMES{$module_name}.temp" or
    die "Cannor read from $FILE_NAMES{$module_name}.temp, $!\n";
  open MODULE_FILE, ">$FILE_NAMES{$module_name}" or
    die "Cannot write to $FILE_NAMES{$module_name}, $!";
  while(<MODULE_INPUT>) {
    $this_line = $_;
    # the stuff in the second () is the option to match on.
    my ($cust_module_name, $option, $rest_of_line) 
      = m {
	   ^
	   \*			# Match the first character as a '*'
	   (\w*)		# followed by some number of A/N characters
	   $module_name		# followed by our actual module
				# name. this captures the actual
				# module name as it might be linked to
				# allow for multiple instances of the module
	   (\w+)		# This is the actual option name
	   (.*)$		# capture and remember the rest of the line
	  }xi;
 
    unless (defined $option and exists $module_options{$option}) {
      # this means that the line of the wharf file is not interesting
      # so, we will just print it out unmodified
      print MODULE_FILE;
      next;
    }
    # We know we have a potential line to replace.  
    # First, we want to check to see if this looks just like what we would
    # replace it with.
    $faux_line = "*" . $cust_module_name . $module_name .
      $option . " " .$module_options{$option};
    $faux_line =~ tr /A-Z/a-z/;	# Normalize to lower case
    $faux_line =~ s/\s+/ /g;	# Remove extra spaces
    $this_line =~ tr/A-Z/a-z/;	# normalize this line to all lower case.
    $this_line =~ s/\s+/ /g;
    chomp $this_line;
    chomp $faux_line;
    if ($faux_line eq $this_line) {
      print MODULE_FILE;
      next;
    }
    my $new_line = "*${cust_module_name}${proper_name}${option} $module_options{$option}";
    if ($option =~ /Image$/) {
      $rest_of_line =~ s/^\s+//;
      my ($desk, $image) = split /\s+/, $rest_of_line, 2;
      $new_line = "*${cust_module_name}${proper_name}${option} $desk $module_options{$option}{$desk}\n";
    }
    # To get here means that $this_line matches one of the lines
    # specified in the ${THEMES_DIR}/wharf.${THEME_NAME} file
    # So, let's just commend out the line with a message
    print MODULE_FILE "# next line commented out by theme.${THEME_NAME}\n";
    s/^/\#/;
    print MODULE_FILE;	# Send the current line to file commented out
    # Now write the themefied line out..
    print MODULE_FILE $new_line;
  }
    close MODULE_INPUT;
    close MODULE_FILE;
    unlink "$FILE_NAMES{$module_name}.temp" or
      warn "Unable to delete $FILE_NAMES{$module_name}.temp, $!\n";
}

sub fix_path {
  # It is possible for images to have a complete path specified in the
  # theme pack.  However, all the images specified in the
  # configuration files have been stored in the theme dir.  Therefore,
  # the specific path that might be specified needs to be stripped
  # off.  This subroutine will return a parameter line that has the
  # path stripped.  It is up to the client to use it instead of the
  # origianl parameter line.

  my $option = shift;
  my $image_file = shift;

  # The first thing to do is make sure this is not the background
  # image for the Pager.  That is the only image allowed to have a
  # path attached.  All others will be deleted.  And, the only
  # path allowed for the DesktopImage is to $NON-CONFIG_DIR.
  if ($option eq "DesktopImage" or $option eq "Image") {
      # These two options will be left alone if they have the default
      # path specified for the image.

    $image_file =~ s!^(.*\/)!!;
    my $path = $1;
    # I don't know how to make Perl grok the '~' when doing file
    # lookups, so let's convert it.
    $path =~ s/^~/$ENV{'HOME'}/;
    if ($path ne "${NONCONFIG_DIR}/") {
      # There is some "non-standard" path specified
      $path = $THEME_DIR;
    } else {
      # The default path is specified, but I need to temporarily
      # remove the leading '/'
      $path = substr $path, 0, -1;
    }
    # now put the image back together...
    $image_file = $path . "/" . $image_file;
  } else {
    # This means we have a "standard" image option and need to
    # strip any possible specifying path off
    $image_file =~ s!^(.*\/)!!;
  }

  # This is dumb, but I need to convert $HOME back to a ~
  $image_file =~ s/^$ENV{'HOME'}/~/;
  return $image_file;
}
