#!/usr/bin/perl -w

use strict;
use Cwd;
use POSIX qw(tmpnam);
use File::Copy;
# use Data::Dumper;		# XXX: debug only
package main;

# These globals comprise the database of all the effects and params in
# both the old (4.x) and new (5) versions, and the commands to
# convert from old to new.

# The params hashes are indexed like:
#   $params{plugin-name}{effect-number}{param-name}
#   The result of this is a hash containing:
#     num (a number, spark-API style, first button on p.1 is #6),
#     type (string), and maybe val (number).
# The buttons hashes are indexed the same way but go from button
# number to param name.
# The effects hashes allow bidirectional lookup:
#   $effects{plugin-name}{effect-number} => effect-name, and
#   $effects{plugin-name}{effect-name} => effect-number.
# Cmds is indexed only by plugin, because some of the commands are not
# effect-specific (e.g. move_effect).  Each element of the cmds hash
# is an array of commands, to be applied in order (if it applies at
# all); each command is a hash containing elements specific to each
# command type, but always include 'type' (string), and generally
# include effect (number), effect_name (string), and perhaps oldnum,
# newnum and val. 
my %oldparams = ();
my %newparams = ();
my %oldbuttons = ();
my %newbuttons = ();
my %oldeffects = ();
my %neweffects = ();
my %cmds = ();			# hash of hashes; index by plugin

my $print_commands = 0;		# turned on by cmdline -print-cmds

my $current_file = "";		# current filename, for msgs.
my $file_name_printed = 0;	# have we printed current filename?
my $debug = 1;                  # XXX Put back when done
my $recurse_p = 0;
my $backup_p = 0;
my $n_converted = 0;

# XXX current_frame_w/h only used for 
# guessing the aspect ratio
# for correcting blur_width/blur_rel_x in v5.
my $current_frame_width = 0;
my $current_frame_height = 0;

my $ignoreversion = "2 or 3";
my $oldversion = "4";
my $newversion = "5";
my $ignoredir = "sapphire_2.0|sapphire_3.0"; # regex XXX:TEST
my $olddir = "sapphire_4";
my $newdir = "sapphire_5";

# For plug-ins that have been renamed, map old name -> new name
my %renames = (
    # example:
    # "S_Lens" => "S_Distort",
    );

# For effects (not plug-ins) that have been renamed, map old name -> new name
# See readparam.
my %effect_renames = (
    # example:
    # "Lens" => "Distort",
    );

# Renamed params within a plug-in; this is a hash of plugin_regex => (old => new).
# See readparam.
my %param_renames = (
    # Glint/Glare/Glow.  Note: Comp versions don't change.
    # example:
    # 'S_(Glint|Glare|Glows?)(Ma?sk)?$' => { "scale_back" => "scale_source" },
    'S_BandPass' => {"offset" => "offset_darks"}
    );

# List of all known cmd types:
# remove_effect
# move_effect
# remove_param
# move_param
# add_param
# add_param_to_param


sub Warn
{
    if (!$file_name_printed) {
	warn ("$current_file:\n  ...", @_);
	$file_name_printed = 1;
    }
    else {
	warn "  ...", @_;
    }
}

sub Print
{
    if (!$file_name_printed) {
	print ("$current_file:\n  ...", @_);
	$file_name_printed = 1;
    }
    else {
	print "  ...", @_;
    }
}


sub get_tmpfile 
{
    my $tempfile = tmpnam();
    return $tempfile;
}

sub read_param_files
{
    my($oldname, $newname) = @_;

    # Read in old params
    open(OLDP, $oldname) || die "ERROR: can't open database file $oldname: $!.\n";
    while (<OLDP>) {
	readparam($_, \%oldparams, \%oldbuttons, \%oldeffects, 1);
    }
    close(OLDP);

    # Read in new params
    open(NEWP, $newname) || die "ERROR: can't open database file $newname: $!.\n";
    while (<NEWP>) {
	readparam($_, \%newparams, \%newbuttons, \%neweffects, 0);
    }
    close(NEWP);
}

sub compute_change_cmds
{
# First look through the old params, finding all that have been moved
# (same name, different button) or removed (no such param name anymore)
  PLUGIN:
    for my $plugin (sort keys %oldparams) {
	if (!defined($newparams{$plugin})) {
	    # Plugin deleted
	    printf("DELETED_PLUGIN $plugin\n");
	    next PLUGIN;
	}
      EFFECT:
	for my $effect (sort keys %{ $oldparams{$plugin}}) {
	    my $newnum = $effect;
	    my $effectname = $oldeffects{$plugin}{$effect};
	    if (!defined($neweffects{$plugin}{$effectname})) {
		# No new effect by that name
		push(@{$cmds{$plugin}},
		     {type => 'remove_effect',
		      effect_name => $effectname,
		      effect => $effect});
		next EFFECT;
	    }
	    elsif ($effectname ne $neweffects{$plugin}{$effect}) {
		# effect name at this number changed
		$newnum = $neweffects{$plugin}{$effectname};
		push(@{$cmds{$plugin}},
		     {type => 'move_effect',
		      effect_name => $effectname,
		      effect => $effect,
		      newnum => $newnum});
	    }
	    for my $param (sort keys %{ $oldparams{$plugin}{$effect}}) {
		my $newparam = $param;

		my ($oldbutton, $newbutton, $oldtype, $newtype);
		$oldbutton = $oldparams{$plugin}{$effect}{$param}{"num"};
		$oldtype = $oldparams{$plugin}{$effect}{$param}{"type"};
		if (defined($newparams{$plugin}{$newnum}{$newparam})) {
		    $newbutton = $newparams{$plugin}{$newnum}{$newparam}{"num"};
		    $newtype = $newparams{$plugin}{$newnum}{$newparam}{"type"};
		}
		if ($param =~ /^wenable/) {
		    # old param is a widget enable; turn it off in the new setup
		    push(@{$cmds{$plugin}},
			 {type => 'remove_param',
			  effect_name => $effectname,
			  effect => $effect,
			  name => $newparam,
			  oldnum => $oldbutton,
			  val => 0
			  });
		}
		elsif (!defined($newbutton)) {
		    push(@{$cmds{$plugin}},
			 {type => 'remove_param',
			  effect_name => $effectname,
			  effect => $effect,
			  name => $newparam,
			  oldnum => $oldbutton,
			  val => 0
			  });
		}
		elsif ($oldbutton != $newbutton) {
		    # Remove the old button, and move it to new one.
		    push(@{$cmds{$plugin}},
			 {type => 'remove_param',
			  effect_name => $effectname,
			  effect => $effect,
			  name => $newparam,
			  oldnum => $oldbutton,
			  val => 0
			  });
		    push(@{$cmds{$plugin}},
			 {type => 'move_param',
			  effect_name => $effectname,
			  name => $newparam,
			  effect => $effect,
			  oldnum => $oldbutton,
			  newnum => $newbutton});
		}

		# This is outside the else on purpose -- we can 
		# move and change type for the same param
		if (defined($newbutton) && $oldtype ne $newtype) {
		    # Change the param type (only works for yes/no popup -> bool at the moment)
		    push(@{$cmds{$plugin}},
			 {type => 'change_param_type',
			  effect_name => $effectname,
			  name => $newparam,
			  effect => $effect,
			  oldnum => $oldbutton,
			  newnum => $newbutton,
			  oldtype => $oldtype,
		          newtype => $newtype});
		}
	    }
	}
    }

    # Then look through the new params, finding all the new plugins, new
    # effects, and new params.
  PLUGIN:
    for my $plugin (sort keys %newparams) {
	if (!defined($oldparams{$plugin})) {
	    #print("NEW_PLUGIN   $plugin\n");
	    next PLUGIN;
	}
      EFFECT:
	for my $effect (sort keys %{ $newparams{$plugin}}) {
	    my $oldnum = $effect;
	    my $effectname = $neweffects{$plugin}{$effect};
	    if (!defined($oldeffects{$plugin}{$effectname})) {
		#print("NEW_EFFECT   $plugin:$effectname($effect)\n");
		next EFFECT;
	    }
	    elsif ($effect ne $oldeffects{$plugin}{$effectname}) {
		# effect name at this number changed
		$oldnum = $oldeffects{$plugin}{$effectname};
	    }

	    for my $param (sort keys %{ $newparams{$plugin}{$effect}}) {
		# print "Checking new $plugin.$effect($effectname).$param\n";

		my $oldparam = $param;

		my $oldbutton = $oldparams{$plugin}{$oldnum}{$oldparam}{"num"};
		my $newbutton = $newparams{$plugin}{$effect}{$param}{"num"};
		if (!defined($oldbutton)) {
		    if ($param eq 'exposure_bias') {
			push(@{$cmds{$plugin}},
			     {type => 'convert_exposure_bias',
			      effect_name => $effectname,
			      effect => $effect,
			      name => $param,
			      from_brt => $oldparams{$plugin}{$oldnum}{'from_rel_bright'}{'num'},
			      to_brt => $oldparams{$plugin}{$oldnum}{'to_rel_bright'}{'num'},
			      newnum => $newbutton});
			# Now get rid of the remove_param commands for from/to
#		  @newcmds = grep {!($_->{type} eq 'remove_param' &&
#				     $_->{name} =~ /rel_bright$/)} @{$cmds{$plugin}};
#		  $cmds{$plugin} = \@newcmds; # replace old ones
		    }
		    else {
			if (!defined($newparams{$plugin}{$effect}{$param}{'val'})) {
			    print "ERROR: Undefined 'val' for new $plugin.$effect.$param\n";
			}
			if (!defined($newparams{$plugin}{$effect}{$param}{'type'})) {
			    print "ERROR: Undefined 'type' for new $plugin.$effect.$param\n";
			}
			if (!defined($newbutton)) {
			    print "ERROR: Undefined newbutton for new $plugin.$effect.$param\n";
			}
			my $v = $newparams{$plugin}{$effect}{$param}{'val'};
			my $type = $newparams{$plugin}{$effect}{$param}{'type'};
			if ($type eq 'COLOR') {
			    my ($green, $blue);
			    $green = $newparams{$plugin}{$effect}{$param}{'color_green'};
			    $blue = $newparams{$plugin}{$effect}{$param}{'color_blue'};
			    if (!defined($green) || !defined($blue)) {
				print "ERROR: green/blue not defined for color param $plugin.$effectname.$param ($newparams{$plugin}{$effect}{$param})\n";
			    }
			    $v = pack_color($v,$green,$blue);
			    print "$plugin.$effect.$param: color param green=$green, blue=$blue: =$v\n" if $debug;
			}
			push(@{$cmds{$plugin}},
			     {type => 'add_param',
			      effect_name => $effectname,
			      effect => $effect,
			      name => $param,
			      button_type => $type,
			      newnum => $newbutton,
			      val => $v,
			  });
		    }
		}
	    }
	}
    }
}

my %do_order = (
		'remove_effect' => 1,
		'move_effect' => 2,
		'remove_param' => 3,
		'move_param' => 4,
		'add_param' => 5,
		'convert_exposure_bias' => 6,
		'change_param_type' => 7,
		'add_param_to_param' => 8,
		);

sub sort_order 
{
    $do_order{$a->{type}} <=> $do_order{$b->{type}};
}

# Print out all cmds (for debugging)
sub print_cmds 
{
    print "=======START\n";

    for my $plugin (sort keys %cmds) {
	print " === $plugin:\n";
	# Sort the commands so they're executed in proper order
	my @allcmds = @{$cmds{$plugin}};
	@allcmds = sort sort_order @allcmds;
	foreach my $cmd (@allcmds) {
	    my $t = $cmd->{type};
	    if ($t eq 'remove_effect') {
		printf("    remove effect %s(%d) (XXX: nds special treatment)\n",
		       $cmd->{effect_name},
		       $cmd->{effect});
	    }
	    elsif ($t eq 'move_effect') {
		printf("    move effect %s from %d to %d\n",
		       $cmd->{effect_name},
		       $cmd->{effect},
		       $cmd->{newnum});
	    }
	    elsif ($t eq 'move_param') {
		printf("    move param %s(%d):%s from %d to %d\n",
		       $cmd->{effect_name},
		       $cmd->{effect},
		       $cmd->{name},
		       $cmd->{oldnum},
		       $cmd->{newnum});
	    }
	    elsif ($t eq 'add_param') {
		printf("    add param %s(%d):%s, button %d (set to %g)\n",
		       $cmd->{effect_name},
		       $cmd->{effect},
		       $cmd->{name},
		       $cmd->{newnum},
		       $cmd->{val});
	    }
	    elsif ($t eq 'remove_param') {
		printf("    remove param %s(%d):%s from button %d (set type to OFF)\n",
		       $cmd->{effect_name},
		       $cmd->{effect},
		       $cmd->{name},
		       $cmd->{oldnum});
	    }
	    elsif ($t eq 'convert_exposure_bias') {
		printf("    set exp bias val %s(%d):%s (button %d) from %d and %d\n",
		       $cmd->{effect_name},
		       $cmd->{effect},
		       $cmd->{name},
		       $cmd->{newnum},
		       $cmd->{from_brt},
		       $cmd->{to_brt});
	    }
	    elsif ($t eq 'change_param_type') {
		printf("    change param %s(%d):%s from type %s to %s\n",
		       $cmd->{effect_name},
		       $cmd->{effect},
		       $cmd->{name},
		       $cmd->{oldtype},
		       $cmd->{newtype});

	    }
	    elsif ($t eq 'add_param_to_param') {
		printf("    add param %s(%d):%s's value (*%g) to %s (if no curves)\n",
		       $cmd->{effect_name},
		       $cmd->{effect},
		       $cmd->{src},
		       $cmd->{scale},
		       $cmd->{dst});
	    }
	    else {
		# unknown cmd type
		my $str = '';
		foreach my $key (grep {!/type/} keys %$cmd) {
		    my $val = $cmd->{$key};
		    $str .= "$key is $val, ";
		}
		print "   UNKNOWN CMD: $plugin: $t: $str\n";
	    }
	}
    }
    print "=======DONE\n";
}

my $lastparam;

sub readparam
{
    my($line, $paramref, $buttonref, $effectref, $old_p) = @_;
    my ($plugin, $effect_num, $effect_name, $param,
	$button_type, $button_number, $val, , $ignore_this);
    $line =~ s/Dest Frame Cycle\s*:/Dest Frame Cycle/;
    if ($line =~ m/([^|]+)\| # plugin, followed by |
	\s*([0-9]+)\. # effect number, followed by .
	\s*([^|]+)\|  # effect name, followed by |
	\s*([^|]+)\|  # param name, followed by |
	\s*([^ ]+)\s* # param type
	button\s*([-0-9]+),  # button number
	\s*val=([-0-9.+e]+)$  # value (a float)
	/x) {
	($plugin, $effect_num, $effect_name, $param,
	 $button_type, $button_number, $val) = ($1, $2, $3, $4, $5, $6, $7);
    }
    else {
	print ("ERR: Can't match regexp on line:\n  $line");
    }
    
    # Handle renamed plug-ins (partly; more done elsewhere) by renaming
    # the old plug-in name here
    if (defined($renames{$plugin})) {
	$plugin = $renames{$plugin};
    }
    # Handle effect renames:
    if ($old_p && defined($effect_renames{$effect_name})) {
	$effect_name = $effect_renames{$effect_name};
    }

    # Here's where we handle renaming of params.  These won't have any
    # effect on the output if we do them right, unless the buttons
    # move also.  (Because the setups are stored by button number.)
    # We just rename the old params as we're reading them in, so
    # they'll match the new ones.
    if ($old_p) {
	for my $plugin_regex (keys %param_renames) {
	    if ($plugin =~ /$plugin_regex/i) {
		my $newparam = $param_renames{$plugin_regex}{$param};
		if (defined($newparam)) {
		    print "Rename param $plugin.$effect_name.$param to $newparam\n"
			if $debug;
		    $param = $newparam;
		}
	    }
	}
    }
	    
    if ($button_type eq "color_blue") {
	#print "Assigning $param.$effect_name.$param: lastparam blue = $val (last=$lastparam)\n";
	$lastparam->{"color_blue"} = $val;
	# don't bother with ignore_this since we're just returning here anyway.
	return;
    }
    elsif ($button_type eq "color_green") {
	#print "Assigning $plugin.$effect_name.$param: lastparam green = $val (last=$lastparam)\n";
	$lastparam->{"color_green"} = $val;
	$param = "$param.label";
	$button_type = 'float';	# really label
	$ignore_this = 1;
    }

    $button_type = 'color' if ($button_type =~ /color/);
    $button_type = 'pup' if ($button_type eq 'popup');
    $button_type = 'bool' if ($button_type eq 'boolean');
    $button_type =~ tr/a-z/A-Z/;

    $param =~ s/%/_amt/;	# old special case for v2 "wipe%"

#    printf("Setting %s : %s(%d) : %s -> { num => %s, type => %s }\n",
#		$plugin, $effect_name, $effect_num, $param,
#		$button_number, $button_type);
    $$paramref{$plugin}{$effect_num}{$param} =
    {"num" => $button_number + 6,
     "type" => $button_type,
     "val" => $val};
    $lastparam = $$paramref{$plugin}{$effect_num}{$param} unless $ignore_this; # keep old lastparam for colors
    $$buttonref{$plugin}{$effect_num}{$button_number+6} = $param;
    # Effect stores bidir mapping from effect #s to effect names.
    # (there's no conflict since effect names are never numbers.)
    # We don't really need to set this every time, but it's easier
    # than checking.
    $$effectref{$plugin}{$effect_num} = $effect_name;
    $$effectref{$plugin}{$effect_name} = $effect_num;
}

# A setup is a hash of four elements: preamble (a string), postamble
# (another string), an array of buttons indexed by button number
# (indexed the spark API way, with 6 being the first button on page
# 1), and an array (usually mostly empty) of channels (each of which
# is a string).
# Each button is a hash containing type and val.

# Parse a setup file into the setup array.
# Returns 1 normally, 0 on error.
sub parse_setup 
{
    my($file, $setup) = @_;
    %{$setup} = ();
    $setup->{filename} = $file;	# remember the name for later err msg printing.
    $setup->{preamble} = "";
    my($state)='begin';
    my($button)=-1;		# points to the current button being read
    if (!open(FILE, $file)) {
	Warn "ERROR: Can't read setup file $file: $! ".
	    "(continuing with other setups...)\n";
	return 0;
    }
    while (<FILE>) {
	s/^\s+//g;		# trim initial whitespace

	# Used for BLUR_USE_PIXEL_ASPECT
	# get the frame width and height
	if (/^frameWidth (\d+)/) {
	    $current_frame_width = $1;
	}
	if (/^frameHeight (\d+)/) {
	    $current_frame_height = $1;
	}

	if ($state eq 'begin') {
	    # Haven't seen trigger yet
	    $setup->{preamble} .= $_;
	    if (/^frameWidth/) {
		$state = 'maybeButtons';
	    }
	}
	elsif ($state eq 'readingSetupCtrls') {
	    $setup->{preamble} .= $_;
	    if (/^EndSetupCtrls/) {
		$state = 'buttons';
	    }
	}
	elsif ($state eq 'maybeButtons') {
	    if (/^BeginSetupCtrls/) {
		$setup->{preamble} .= $_;
		$state = 'readingSetupCtrls';
	    }
	    else {
		$button++;
		$setup->{buttons}[$button] = parse_button($_);
		$state = 'buttons';
	    }
	}
	elsif ($state eq 'buttons') {
	    # read a button
	    if (/^SPARK_CHANNEL/) {
		$setup->{channels}[$button] = $_;
		$state = 'channel';
	    }
 	    elsif (/^SPARK/) {
 		$button++;
 		$setup->{buttons}[$button] = parse_button($_);
 	    }
	    elsif (/^End/) {
		$setup->{postamble} .= $_;
		$state = 'end';
	    }
	    elsif (/^\s*$/) {
		# blank line: do nothing
	    }
	    else {
		Warn "Unexpected info at $file, line $.. Expected SPARK_* or End.\n";
		close FILE;
		return 0;
	    }
	}
	elsif ($state eq 'channel') {
	    # reading a channel
	    if (/^SPARK/) {
		$button++;
		$setup->{buttons}[$button] = parse_button($_);
		$state = 'buttons';
	    }
	    else {
		$setup->{channels}[$button] .= $_;
	    }
	}
	elsif ($state eq 'end') {
	    $setup->{postamble} .= $_;
	}
    }
    close FILE;
    if ($state eq 'begin') {
	return 0;		# error: nothing found.
    }
    else {
	return 1;		# success
    }
}

# Takes an input line and returns a hash representing that button
sub parse_button 
{
    my($line) = @_;
    chop($line);
    if ($line =~ /^SPARK_([A-Z_]+)\s*([-0-9.]+)/) {
	return {type=>$1, val=>$2};
    }
    if ($line =~ /^SPARK_([A-Z_]+)/) {
	return {type=>$1};
    }
    else {
	print ("Unknown button type $line\n");
	return {type=>'unknown', val=>''};
    }
}

# XXX: Check for each new version
sub has_on_fields_p 
{
    my ($plugin) = @_;
    return !($plugin eq "Temporal" ||
	     $plugin eq "FieldTool" ||
	     $plugin eq "FieldRemove" ||
	     $plugin eq "Feedback" ||
	     $plugin eq "FeedbackMatte");
}

# Return the highest button number used by the plugin
# Returns a Spark button number (starting at 6)
sub max_plugin_button
{
    my ($plugin) = @_;
    my ($max) = 0;

    for my $effect (keys %{$newbuttons{$plugin}} ) {
	for my $button (keys %{$newbuttons{$plugin}{$effect}}) {
	    $max = $button if ($button > $max);
	}
    }
    return $max;
}

# Returns 1 if this button number is a global button (for this plugin;
# there are certain plugins which are different.)
sub global_button_p 
{
    my($plugin, $button) = @_;	# plugin name (str), button # (int)
    if ($button == 6 && defined($neweffects{$plugin}{1})) { # effect popup
	return 1;
    }

    if ((max_plugin_button($plugin) < 64 && $button > 63) ||
	$button > 92) {
	return 1;		# everything on the crop page (3 or 4) is a global
    }
    # These plug-ins have no On Fields button
    if (!has_on_fields_p($plugin) &&
	($button == 30 || $button == 59)) {
	return 0;
    }
    
    if (($button == 31 || $button == 60 || # redraw wait
	 (has_on_fields_p($plugin) &&
	  ($button == 30 || $button == 59)))) { # on fields
	return 1;
    }
    if ($button == 32 || $button == 61) { # undo
	return 1;
    }
    if ($button == 33 || $button == 62) { # ld defaults
	return 1;
    }
    if ($button == 34 || $button == 63) { # res factor
	return 1;
    }

    return 0;
}

# Check all the new params to see if the given button is present in
# any effect of that plugin.
sub button_present_in_any_effect_p 
{
    my($plugin, $button) = @_;
    foreach my $effect (keys %{$newparams{$plugin}}) {
#	print "button_present_in_any_effect_p: checking " .
#	    "$plugin . $effect for button $button\n";
	return 1 if (defined($newbuttons{$plugin}{$effect}{$button}));
    }
#    print "... $button is not present in any effect.\n";
    return 0;
}

sub has_channel
{
    my ($setup, $button) = @_;
    my $channel = $setup->{channels}[$button];
    return 0 if (!defined($channel));
    return 1 if $channel =~ /Key [0-9].*Key [0-9]/s; # two different keys
    return 0;			# assume only one keyframe, OK to bash
}

# Apply a scale and offset to a channel of a button in the setup
# (which should be a ref to a setup).
# This bashes the setup's channel string in-place.
sub fix_channel_range 
{
    my ($setup, $button, $scale, $offset) = @_;
    my $channel = $setup->{channels}[$button];
    return if (!defined($channel));
    $channel =~ s/(\sValue)\s+(-?[0-9.]+)/"$1 ".($2 * $scale + $offset)/ge;
    $channel =~ s/(\s(Left|Right)Slope)\s+(-?[0-9.]+)/"$1 ".($3*$scale)/ge;
    $setup->{channels}[$button] = $channel;
}

sub process_setup 
{
    my($setup, $newsetup) = @_;
    
    # Get the name of the plugin from the preamble
    my ($plugin) = $setup->{preamble} =~ m{Name .*/([^/]+)\.spark(_x86_64|_64)?\.setup};

    %{$newsetup} = ();
    $newsetup->{preamble} = $setup->{preamble};
    $newsetup->{preamble} =~ s/$olddir/$newdir/g;
    for my $old ( keys %renames) {
	my $new = $renames{$old};
	if ($plugin eq $old) {
	    $newsetup->{preamble} =~ s/$old/$new/;
	    $plugin = $new;
	}
    }
    $newsetup->{postamble} = $setup->{postamble};
    $newsetup->{filename} = $setup->{filename}." (converted to $newversion)";

    # Copy the buttons and channels over; we'll modify them later.
    for (my $button = 0; $button < @{$setup->{buttons}}; $button++) {
	# the extra { } create a (ref to a) copy of the button hash.
	$newsetup->{buttons}[$button] = { %{$setup->{buttons}[$button]} }
	if (defined($setup->{buttons}[$button]));

	$newsetup->{channels}[$button] = $setup->{channels}[$button]
	    if (defined($setup->{channels}[$button]));
    }


    # Get which effect it is: if there's a popup at button 6 and
    # effect #1 (2nd effect) is defined, it's the effect popup.  
    my $effect = 0;
    if (defined($setup->{buttons}[6]) &&
	defined($neweffects{$plugin}{1}) &&
	$setup->{buttons}[6]{type} eq 'PUP') {
	$effect = $setup->{buttons}[6]{val};
    }
    my $orig_effect = $effect;

    # now apply the commands

    # Sort the commands so they're executed in proper order
    my @allcmds = defined($cmds{$plugin}) ? @{$cmds{$plugin}} : ();
    @allcmds = sort sort_order @allcmds;
    foreach my $cmd (@allcmds) {
	my $t = $cmd->{type};

	# We only execute commands for the current effect,  or remove_params
	# cmds for other effects if there's no such button in this (old) effect.
	# This prevents leaving the buttons set to wrong values or types.
	# Otherwise we just skip this command.
	if (!($cmd->{effect} == $effect ||
	      ($t eq 'remove_param' &&
	       !defined($oldbuttons{$plugin}{$effect}{$cmd->{oldnum}})))) {
	    next;
	}

	if ($t eq 'remove_effect') {
	    # This probably only works for LensFlareChroma -> LensFlare.
	    printf("    remove effect %s(%d) (XXX: nds special treatment)\n",
		   $cmd->{effect_name},
		   $cmd->{effect}) if ($debug);
	    $effect = 0;	# XXX: if not lensflare, change this.
	}
	elsif ($t eq 'move_effect') {
	    # Always check the original effect number before doing
	    # move_effect, to prevent cascading effect-changes.
	    next if ($cmd->{effect} != $orig_effect);
	    printf("    move effect %s from %d to %d\n",
		   $cmd->{effect_name},
		   $cmd->{effect},
		   $cmd->{newnum}) if ($debug);
	    $effect = $cmd->{newnum};
	    if ($setup->{buttons}[6]{type} ne 'PUP' &&
		$setup->{buttons}[6]{type} ne 'OFF') {
		my $msg =
		    sprintf("ERROR: $plugin: button 6 is %s.  ".
			    "Should be POPUP or OFF.\n",
			    $setup->{buttons}[6]{type});
		die($msg);
	    }
	    $newsetup->{buttons}[6]{type} = 'PUP';
	    $newsetup->{buttons}[6]{val} = $effect;
	}
	elsif ($t eq 'move_param') {
	    printf("    move param %s(%d):%s from %d to %d\n",
		   $cmd->{effect_name},
		   $cmd->{effect},
		   $cmd->{name},
		   $cmd->{oldnum},
		   $cmd->{newnum}) if ($debug);
	    # Copy button and channel
	    $newsetup->{buttons}[$cmd->{newnum}] =
	    { %{$setup->{buttons}[$cmd->{oldnum}]} };
	    if (defined($setup->{channels}[$cmd->{oldnum}])) {
		# Use the new button number for the moved channel (replace
		# the number after SPARK_CHANNEL with the new number)
		($newsetup->{channels}[$cmd->{newnum}] =
		 $setup->{channels}[$cmd->{oldnum}]) =~
		 s/(SPARK_CHANNEL\s*)[0-9]+/$1$cmd->{newnum}/;
	    }
	}
	elsif ($t eq 'add_param') {
	    printf("    add param %s(%d):%s, button %d (set to %s: %g)\n",
		   $cmd->{effect_name},
		   $cmd->{effect},
		   $cmd->{name},
		   $cmd->{newnum},
		   $cmd->{button_type},
		   $cmd->{val}) if ($debug);
	    $newsetup->{buttons}[$cmd->{newnum}] =
	    { type => $cmd->{button_type},
	      val => $cmd->{val},
	      num => $cmd->{newnum}};
	}
	elsif ($t eq 'remove_param') {
	    printf("    remove param %s(%d):%s from button %d (set type to OFF)\n",
		   $cmd->{effect_name},
		   $cmd->{effect},
		   $cmd->{name},
		   $cmd->{oldnum}) if ($debug);
	    $newsetup->{buttons}[$cmd->{oldnum}] =
	    { type => 'OFF',
	      val => '',
	      num => $cmd->{oldnum}};
	    $newsetup->{channels}[$cmd->{oldnum}] = '';
	}
	elsif ($t eq 'change_param_type') {
	    printf("    change param %s(%d):%s from %s to %s\n",
		   $cmd->{effect_name},
		   $cmd->{effect},
		   $cmd->{name},
		   $cmd->{oldtype},
		   $cmd->{newtype}) if ($debug);
	    if ($cmd->{oldtype} ne 'PUP' || $cmd->{newtype} ne "BOOL") {
		print "Warning in $plugin.$cmd->{effect_name}.$cmd->{name}: type conversion from $cmd->{oldtype} to $cmd->{newtype} " .
		    "is not supported\n";
	    }
	    $newsetup->{buttons}[$cmd->{newnum}]{type} = $cmd->{newtype};
	}
	elsif ($t eq 'convert_exposure_bias') {
	    printf("    set exp bias val %s(%d):%s (button %d) from %d and %d\n",
		   $cmd->{effect_name},
		   $cmd->{effect},
		   $cmd->{name},
		   $cmd->{newnum},
		   $cmd->{from_brt},
		   $cmd->{to_brt}) if ($debug);
	    my $from = $setup->{buttons}[$cmd->{from_brt}]{val};
	    my $to = $setup->{buttons}[$cmd->{to_brt}]{val};
	    my $bias = $to / ($from + $to);
	    $newsetup->{buttons}[$cmd->{newnum}] =
	    { type => 'FLOAT',
	      val => $bias,
	      num => $cmd->{newnum}};
	    # Delete the channels for both (this is probably redundant)
	    $newsetup->{channels}[$cmd->{from_brt}] = '';
	    $newsetup->{channels}[$cmd->{to_brt}] = '';
	}
	elsif ($t eq 'add_param_to_param') {
	    if (has_channel($newsetup, $cmd->{src}) ||
		has_channel($newsetup, $cmd->{dst})) {
		printf("    %s: add button %d's value to %d: IGNORING due to channels: '%s', '%s'\n",
		       $cmd->{effect_name},
		       $cmd->{src},
		       $cmd->{dst},
		       $cmd->{scale},
		       $newsetup->{channels}[$cmd->{src}],
		       $newsetup->{channels}[$cmd->{dst}]) if ($debug);
	    }
	    else {
		if (defined($setup->{buttons}[$cmd->{dst}])) {
		    my $src = $setup->{buttons}[$cmd->{src}]{val};
		    my $dst = $setup->{buttons}[$cmd->{dst}]{val};
		    printf("    %s: add button %d's value (=%g) * %g to button %d (=%g)\n",
			   $cmd->{effect_name},
			   $cmd->{src},
			   $src, $cmd->{scale},
			   $cmd->{dst},
			   $dst) if ($debug);
		    $setup->{buttons}[$cmd->{dst}] = $dst + $src * $cmd->{scale};
		    $setup->{channels}[$cmd->{src}] = '';
		    $setup->{channels}[$cmd->{dst}] = '';
		}
	    }
	}
	else {
	    # unknown cmd type
	    if ($debug) {
		my $str = '';
		foreach my $key (grep {!/type/} keys %$cmd) {
		    my $val = $cmd->{$key};
		    $str .= "$key is $val, ";
		}
		print "   UNKNOWN CMD: $plugin: $t: $str\n";
	    }
	}
    }

    # Now turn off all buttons in the new setup for which there is no
    # longer a definition.   This will turn off widget enables as
    # well, but FFFIS will load a setup which has OFFs where there are
    # actually params defined (but not the other way around - that
    # causes an INCOMPATIBLE SETUP error).
    for (my $button = 0; $button < @{$newsetup->{buttons}}; $button++) {
	next if (global_button_p($plugin, $button));
	my $setup_btype = defined($newsetup->{buttons}[$button]) ?
	    $newsetup->{buttons}[$button]{type} : "OFF";
	if (!button_present_in_any_effect_p($plugin, $button) &&
	    $setup_btype ne "OFF") {
	    $newsetup->{buttons}[$button]{type} = "OFF";
	    $newsetup->{buttons}[$button]{val} = undef;
	    $newsetup->{channels}[$button] = undef;
	    print "  Turned off button $button.\n" if $debug;
	}
    }
    
    # Now check that all button types are correct for the new version.
    for (my $button = 0; $button < @{$newsetup->{buttons}}; $button++) {
	my ($pname, $btype, $setup_btype);
	next if (!defined($newbuttons{$plugin}{$effect}{$button}));
	$pname = $newbuttons{$plugin}{$effect}{$button};
	$btype = $newparams{$plugin}{$effect}{$pname}{type};
	$setup_btype = defined($newsetup->{buttons}[$button]) ?
	    $newsetup->{buttons}[$button]{type} : "OFF";
	# OFF is ok anywhere (the button will just fill in with its default value),
	# and BOOL is ok for a pushbutton param (in fact flame seems to save them that way)
	if ($setup_btype ne $btype && $setup_btype ne "OFF" &&
	    !($setup_btype eq "BOOL" && $btype eq "PUSHBUTTON")) {
	    print "BUTTON TYPE error for $button: setup has $setup_btype, should be $btype.\n";
	}	
    }
    

    # Special cases:
    # Here's where we change the ranges or values of params or move effects.
    # Have to do these after the commands are executed so all the new
    # params are in their proper places.  (Could do it before, but
    # you'd have to use all the 'before' button numbers.)
    # When changing the range or value of a param, we also have to
    # delete any channel curves ("channels") because they're no longer
    # valid.  This is much easier than parsing and modifying them.

    # if ($plugin eq 'S_Gradient') {
    # 	# Scale the add noise param -- this won't be perfect in all cases, since the
    # 	# scaling in the effect is per-channel, but it's as close as we can get.
    # 	my $button = $newparams{$plugin}{$effect}{add_noise}{num};
    # 	$newsetup->{buttons}[$button]{val} *= 256;
    # 	fix_channel_range($newsetup, $button, 256, 0);
    # }

    # On Fields:
    # On Fields gets increased by 1 because "On Fields: Auto" got added as the zeroth
    # menu option.
    # On fields is a global, so it can't be looked up by name, and therefore can 
    # only be looked up by button number.  It is 30 in both v4 and b5.
    {
	my $fields_button = 30;
     	my $old_field = $setup->{buttons}[$fields_button]{val};
     	$newsetup->{buttons}[$fields_button]{val} = $old_field + 1;
    }

    # -- Begin Handle frequency params
    # No Scanlines or Rays

    # All Edge Rays (No Rays)
    if ( $plugin =~ m/S_EdgeRays/ ) {
	my $button = $oldparams{$plugin}{$effect}{shimmer_freq}{num};
	my $freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{shimmer_freq}{num};
	$newsetup->{buttons}[$button]{val} = $freq * 4.0;
	fix_channel_range($newsetup, $button, 4.0, 0);
    }
    
    # Warp_Bubble2
    if ( ($plugin =~ m/S_Warps/) and ($neweffects{$plugin}{$effect} =~ m/Bubble2/) ) {
	# A_Frequency
	my $button = $oldparams{$plugin}{$effect}{A_frequency}{num};
	my $freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{A_frequency}{num};
	$newsetup->{buttons}[$button]{val} = $freq * 4.0;
	fix_channel_range($newsetup, $button, 4.0, 0);

	# B_Frequency
	$button = $oldparams{$plugin}{$effect}{B_frequency}{num};
	$freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{B_frequency}{num};
	$newsetup->{buttons}[$button]{val} = $freq * 4.0;
	fix_channel_range($newsetup, $button, 4.0, 0);
    }

    # Shake
    if ( $plugin eq 'S_Shake') {
	# frequency
	my $button = $oldparams{$plugin}{$effect}{frequency}{num};
	my $freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{frequency}{num};
	$newsetup->{buttons}[$button]{val} = $freq * 8.0;
	fix_channel_range($newsetup, $button, 8.0, 0);
	
	#x_rand_freq
	$button = $oldparams{$plugin}{$effect}{x_rand_freq}{num};
	$freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{x_rand_freq}{num};
	$newsetup->{buttons}[$button]{val} = $freq / 8.0;
	fix_channel_range($newsetup, $button, 0.125, 0);

	#y_rand_freq
	$button = $oldparams{$plugin}{$effect}{y_rand_freq}{num};
	$freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{y_rand_freq}{num};
	$newsetup->{buttons}[$button]{val} = $freq / 8.0;
	fix_channel_range($newsetup, $button, 0.125, 0);

	#z_rand_freq
	$button = $oldparams{$plugin}{$effect}{z_rand_freq}{num};
	$freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{z_rand_freq}{num};
	$newsetup->{buttons}[$button]{val} = $freq / 8.0;
	fix_channel_range($newsetup, $button, 0.125, 0);

	#tilt_rand_freq
	$button = $oldparams{$plugin}{$effect}{tilt_rand_freq}{num};
	$freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{tilt_rand_freq}{num};
	$newsetup->{buttons}[$button]{val} = $freq / 8.0;
	fix_channel_range($newsetup, $button, 0.125, 0);
    }
    # Grain Color
    if (  ( $plugin =~ m/S_Grain/) and ($neweffects{$plugin}{$effect} =~ m/Grain Color/)) {
	#color_frequency
	my $button = $oldparams{$plugin}{$effect}{color_frequency}{num};
	my $freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{color_frequency}{num};
	$newsetup->{buttons}[$button]{val} = $freq * 4.0;
	fix_channel_range($newsetup, $button, 4.0, 0);

	#bw_frequency
	$button = $oldparams{$plugin}{$effect}{bw_frequency}{num};
	$freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{bw_frequency}{num};
	$newsetup->{buttons}[$button]{val} = $freq * 4.0;
	fix_channel_range($newsetup, $button, 4.0, 0);	
    }
    # Texture Plasma
    if (  ( $plugin =~ m/S_Textures/) and ($neweffects{$plugin}{$effect} =~ m/Plasma/)) {
	#color_frequency
	my $button = $oldparams{$plugin}{$effect}{noise_frequency}{num};
	my $freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{noise_frequency}{num};
	$newsetup->{buttons}[$button]{val} = $freq * 4.0;
	fix_channel_range($newsetup, $button, 4.0, 0);
    }
    # Texture Noise Paint
    if (  ( $plugin =~ m/S_Textures/) and ($neweffects{$plugin}{$effect} =~ m/Noise Paint/)) {
	#noise_frequency
	my $button = $oldparams{$plugin}{$effect}{noise_frequency}{num};
	my $freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{noise_frequency}{num};
	$newsetup->{buttons}[$button]{val} = $freq * 4.0;
	fix_channel_range($newsetup, $button, 4.0, 0);

	#color_frequency
	$button = $oldparams{$plugin}{$effect}{stroke_frequency}{num};
	$freq = $setup->{buttons}[$button]{val};
	$button = $newparams{$plugin}{$effect}{stroke_frequency}{num};
	$newsetup->{buttons}[$button]{val} = $freq * 32.0;
	fix_channel_range($newsetup, $button, 32.0, 0);
    }
    # CartoonPaint(in S_Cartoon) and all in S_AutoPaint get their 
    # frequency scaled by 32 instead of the 4 above
    if ( (($plugin eq 'S_Cartoon') and ($effect eq 1)) or
	 ($plugin eq 'S_AutoPaint')) {
	print ("Working on $plugin -> $effect\n");
	my $button = $oldparams{$plugin}{$effect}{frequency}{num};
	my $freq = $setup->{buttons}[$button]{val};
	my $newb = $newparams{$plugin}{$effect}{frequency}{num};
	$newsetup->{buttons}[$newb]{val} = $freq * 32.0;	
	fix_channel_range($newsetup, $newb, 32.0, 0);
    }
    # Clouds and CloudsComp
    if ($plugin =~ m/S_Clouds/) {
	if ($neweffects{$plugin}{$effect} =~ m/Mult Color/) {
	    my $button = $oldparams{$plugin}{$effect}{color_freq}{num};
	    my $freq = $setup->{buttons}[$button]{val};
	    $freq = $freq * 2.0;
	    $button = $newparams{$plugin}{$effect}{color_freq}{num};
	    $newsetup->{buttons}[$button]{val} = $freq;
	    fix_channel_range($newsetup, $button, 2.0, 0);
	}
    }



    # These actually call frequency "frquency".
    # Check if it's a plugin that should be changed, then just change once.
    # Probably would be less code to do it the other way, but I have more confidence
    # nothing got missed this way.
    my $update_param_named_freq = 0;
    # Grain Mono
    if ( ( $plugin =~ m/S_Grain/)and ($neweffects{$plugin}{$effect} =~ m/Grain Mono/) ) {
	$update_param_named_freq = 1;	
    }
    # Some Wipes (Not WipeStripes or WipePixelate!) (And no WipePointalize in v4)
    if ( $plugin eq 'S_Wipes' ) {
	if ($neweffects{$plugin}{$effect} =~ m/(Blobs|Cells|Bubble|Clouds|Plasma|Weave)/) {
	    $update_param_named_freq = 1;
	}  
    }
    # Some Dissolves
    # No DissolveEdgeRays in previous version
    # NOT DissolvePixelate
    # DissolveSpeckle and DissolveBubble done later with other special case
    if ( $plugin eq 'S_Dissolves' ) {
	if ($neweffects{$plugin}{$effect} =~ m/Clouds/) {
	    $update_param_named_freq = 1;
	}
    }
    # only warp_bubble.  bubble2 has a different param
    if ( ( $plugin =~ m/S_Warps/) and ($neweffects{$plugin}{$effect} =~ m/Bubble$/) ) {
	$update_param_named_freq = 1;
    }
    # All Clouds
    if ( $plugin =~ m/S_Clouds/) {
	$update_param_named_freq = 1;
    }
    if ( $plugin =~ m/S_Textures/) {
	if ($neweffects{$plugin}{$effect} =~ m/(Folded|Weave|Noise Emboss|Spots|Cells|Chroma Spiral|Neurons|Loops|Flux)/) {
	    $update_param_named_freq = 1;
	} 	
    }
    # Sparkles, SparklesMask, SparklesComp
    if ($plugin =~ m/S_Sparkles/) {
	$update_param_named_freq = 1;
    }
    # PsykoBlobs (Not PsykoZebrafy - that has a frequency that didn't change)
    if (($plugin eq 'S_Psyko') and ($neweffects{$plugin}{$effect} =~ m/Blobs/)){
	$update_param_named_freq = 1;
    }
    # MatteOps
    if ($plugin =~ m/S_MatteOps/) {
	$update_param_named_freq = 1;
    }
    # Feedback Bubble
    if ( ($plugin eq 'S_Feedback') and ($neweffects{$plugin}{$effect} =~ m/Bubble/) ){
	$update_param_named_freq = 1;
    }

    # If it was a plugin to change, with a param called "frequency" change
    if ($update_param_named_freq) {
	my $button = $oldparams{$plugin}{$effect}{frequency}{num};
	if ($button) {
	    my $freq = $setup->{buttons}[$button]{val};
	    $button = $newparams{$plugin}{$effect}{frequency}{num};
	    $newsetup->{buttons}[$button]{val} = $freq * 4.0;
	    fix_channel_range($newsetup, $button, 4.0, 0);
	}
	else {
	    print("$plugin -> $effect didn't have a frequency param...\n");
	}
    }
    $update_param_named_freq = 0;

    # GlowNoise special cased below for other things.  Frequency corrected there.

    # -- End handle frequency params 

    # BandPass had offset change to offset_darks.  Add .5 to old scale value
    if ($plugin eq 'S_BandPass') {
	my $button = $newparams{$plugin}{$effect}{scale}{num};
	$newsetup->{buttons}[$button]{val} += .5;
	# update scale channels
	fix_channel_range($newsetup, $button, 1, .5);

	# updating offset_darks name
	$button = $newparams{$plugin}{$effect}{offset_darks}{num};
	my $channel2 = $setup->{channels}[$button];
	if (defined($channel2) ) {
	    $channel2 =~ s/(\sChannel Offset)/"$1 Darks"/ge;
	    $newsetup->{channels}[$button] = $channel2;
	}
    }

    # In v5, etching got a master control for line frequency.  
    # Adjust the line1 and line2 frequencies to compensate
    if ( ($plugin eq 'S_HalfTone') and ($effect eq 2) ) {
        # scale lines1 and lines2 by line_freq
        # (which should have the default value because it's a new param)

	# get value to scale by
	my $button = $newparams{$plugin}{$effect}{lines_frequency}{num};
	my $lines_freq = $newsetup->{buttons}[$button]{val};

	# scale line1 value
	$button = $newparams{$plugin}{$effect}{lines1_frequency}{num};
	$newsetup->{buttons}[$button]{val} /= $lines_freq;
	# scale line1 channels
	fix_channel_range($newsetup, $button, 1.0 / $lines_freq, 0);

        # scale line2 value
	$button = $newparams{$plugin}{$effect}{lines2_frequency}{num};
	$newsetup->{buttons}[$button]{val} /= $lines_freq;
        # scale line2 channels 
	fix_channel_range($newsetup, $button, 1.0 / $lines_freq, 0);

	# etching also got a master parameter for lines_angle but that defaults to
        # zero so we can just leaves lines1_angle and lines2_angle alone.
    }

    # For S_Mosaic, GlowNoise(S_Glow, S_GlowComp, etc), DissolveSpeckle, DissolveBubble
    # - rel_width was replaced with rel_height, which changes frequency 
    #   and rel_height
    # For S_Moasic:
    # - pixel_frequency gets doubled
    # For GlowNoise:
    # - noise_frequency gets quadrupled (like the other frequencies)
    # Can't easily fix the channel/keyframes for frequency.
    if ($plugin eq 'S_Mosaic') {
	
	# get old frequency
	my $freq_button = $oldparams{$plugin}{$effect}{pixel_frequency}{num};
	my $pix_freq = $setup->{buttons}[$freq_button]{val};

	# get old rel_width
	my $button = $oldparams{$plugin}{$effect}{pixel_rel_width}{num};
	my $pix_wid = $setup->{buttons}[$button]{val};

	# 1) set pixel_height to be the inverse of pixel_width
	# 2) scale pixel_frequency by pixel_width  
	# 3) Finally, scale pixel_frequency by 2.
	my $pix_height_button = $newparams{$plugin}{$effect}{pixel_rel_height}{num};
	my $pix_height = $newsetup->{buttons}[$pix_height_button]{val};
	$pix_height = 1.0 / $pix_wid;
	$pix_freq *= $pix_wid;
	$pix_freq *= 2.0;

	# put new pixel_height back into new setup
	$newsetup->{buttons}[$pix_height_button]{val} = $pix_height;
	# put new pixel_freq back into setup
	$freq_button = $newparams{$plugin}{$effect}{pixel_frequency}{num};
	$newsetup->{buttons}[$freq_button]{val} = $pix_freq;
	
	# clear frequency channel since we can't really updated it correctly
	$newsetup->{channels}[$freq_button] = '';
    }

    # Similar to above, but for GlowNoise(Mask)(Comp)
    if (($plugin =~ m/S_Glow/) and ($neweffects{$plugin}{$effect} =~ m/Noise/  ) ) {
	# get old frequency
	my $freq_button = $oldparams{$plugin}{$effect}{noise_frequency}{num};
	my $noise_freq = $setup->{buttons}[$freq_button]{val};

	# get old rel_width
	my $button = $oldparams{$plugin}{$effect}{noise_freq_rel_x}{num};
	my $noise_x = $setup->{buttons}[$button]{val};

	# 1) set noise_height to be the inverse of noise_width
	# 2) scale noise_frequency by noise_width  
	# 3) Finally, scale noise_frequency by 2.
	my $noise_y_button = $newparams{$plugin}{$effect}{noise_freq_rel_y}{num};
	my $noise_y = $newsetup->{buttons}[$noise_y_button]{val};
	$noise_y = 1.0 / $noise_x;
	$noise_freq *= $noise_x;
	# Correct for base frequency change
	$noise_freq *= 4.0;

	# put new pixel_height back into new setup
	$newsetup->{buttons}[$noise_y_button]{val} = $noise_y;
	# put new pixel_freq back into setup
	$freq_button = $newparams{$plugin}{$effect}{noise_frequency}{num};
	$newsetup->{buttons}[$freq_button]{val} = $noise_freq;

	# clear frequency channel since we can't really updated it correctly
	$newsetup->{channels}[$freq_button] = '';
    }
    # Similar to above, but for DissolveSpeckle and DissolveBubble
    if (($plugin =~ m/S_Dissolves/) and ($neweffects{$plugin}{$effect} =~ m/(Speckle|Bubble)/  ) ) {
	# get old frequency
	my $freq_button = $oldparams{$plugin}{$effect}{frequency}{num};
	my $freq = $setup->{buttons}[$freq_button]{val};

	# get old rel_width
	my $button = $oldparams{$plugin}{$effect}{frequency_rel_x}{num};
	my $x_val = $setup->{buttons}[$button]{val};

	# 1) set height to be the inverse of width
	# 2) scale frequency by width  
	# 3) Finally, scale pixel_frequency by 2.
	my $y_button = $newparams{$plugin}{$effect}{frequency_rel_y}{num};
	my $y_val = $newsetup->{buttons}[$y_button]{val};
	$y_val = 1.0 / $x_val;
	$freq *= $x_val;
	$freq *= 4.0;

	# put new height back into new setup
	$newsetup->{buttons}[$y_button]{val} = $y_val;
	# put new freq back into setup
	$freq_button = $newparams{$plugin}{$effect}{frequency}{num};
	$newsetup->{buttons}[$freq_button]{val} = $freq;

	# clear frequency channel since we can't really updated it correctly
	$newsetup->{channels}[$freq_button] = '';
    }

    if ( ($plugin =~ m/S_Wipes/) and ($neweffects{$plugin}{$effect} =~ m/Stripes/) ) {
	my $button = $oldparams{$plugin}{$effect}{shift}{num};
	my $old_shift = $setup->{buttons}[$button]{val};
	$old_shift = $old_shift * .5;

	$button = $newparams{$plugin}{$effect}{shift}{num};
	$newsetup->{buttons}[$button]{val} = $old_shift;

	fix_channel_range($newsetup, $button, .5, 0);
    }

    # Scale GlowRings brightness down by 5
    if ( ($plugin =~ m/S_Glows/) and ($neweffects{$plugin}{$effect} =~ m/Rings/) ) {
	my $button = $oldparams{$plugin}{$effect}{brightness}{num};
	my $old_bright = $setup->{buttons}[$button]{val};
	$old_bright = $old_bright * .2;

	$button = $newparams{$plugin}{$effect}{brightness}{num};
	$newsetup->{buttons}[$button]{val} = $old_bright;

	fix_channel_range($newsetup, $button, .2, 0);	
    }

    # EdgeColorize (in EdgeDetect) has an inverted rotate
    if ( ($plugin eq 'S_EdgeDetect') and ($neweffects{$plugin}{$effect} =~ m/Colorize/) ) {
	my $button = $oldparams{$plugin}{$effect}{rotate_colors}{num};
	my $rotate = $setup->{buttons}[$button]{val};
	$rotate *= -1.0;

	$button = $newparams{$plugin}{$effect}{rotate_colors}{num};
	$newsetup->{buttons}[$button]{val} = $rotate;

	fix_channel_range($newsetup, $button, -1.0, 0);		
    }

    # Texture Folded: folded_amp in degrees
    if ( ($plugin =~ m/S_Textures/) and ($neweffects{$plugin}{$effect} =~ m/Folded/) ) {
	my $button = $oldparams{$plugin}{$effect}{fold_amp}{num};
	my $amp = $setup->{buttons}[$button]{val};
	$amp *= 180;

	$button = $newparams{$plugin}{$effect}{fold_amp}{num};
	$newsetup->{buttons}[$button]{val} = $amp;

	fix_channel_range($newsetup, $button, 180.0, 0);			
    }

    # XXX do the above related to textures have to also apply to texturecomp?
    if ( ($plugin eq 'S_Textures') and ($neweffects{$plugin}{$effect} =~ m/Noise Emboss/) ) {
	my $button = $oldparams{$plugin}{$effect}{bumps_scale}{num};
	my $bump = $setup->{buttons}[$button]{val};
	$bump *= 2.0;

	$button = $newparams{$plugin}{$effect}{bumps_scale}{num};
	$newsetup->{buttons}[$button]{val} = $bump;

	fix_channel_range($newsetup, $button, 2.0, 0);				
    }
    
    if ($plugin eq 'S_Kaleido')  {
	if ( $neweffects{$plugin}{$effect} =~ m/(Triangles|Diamonds)/ ) {
	    my $button = $oldparams{$plugin}{$effect}{z_dist}{num};
	    my $dist = $setup->{buttons}[$button]{val};
	    $dist *= 2.0;

	    $button = $newparams{$plugin}{$effect}{z_dist}{num};
	    $newsetup->{buttons}[$button]{val} = $dist;

	    fix_channel_range($newsetup, $button, 2.0, 0); 	    
	}
    }

    # MatteOps grow and shink combined
    if ($plugin =~ m/S_MatteOps/) {
	my $button = $oldparams{$plugin}{$effect}{shrink_amount}{num};
	my $shrink = $setup->{buttons}[$button]{val};
	$button = $oldparams{$plugin}{$effect}{grow_amount}{num};
	my $grow = $setup->{buttons}[$button]{val};

	$button = $newparams{$plugin}{$effect}{'shrink-_grow+'}{num};
	$newsetup->{buttons}[$button]{val} = $grow - $shrink;
	# clear frequency channel since we can't really updated it correctly
	$newsetup->{channels}[$button] = '';

    }

    # Sharpen_frequency -> sharpen_width
    if ( ($plugin eq 'S_AutoPaint') or
	 # Both Textures and TexturesComp
	 ( ($plugin =~ m/S_Textures/) and 
	   ($neweffects{$plugin}{$effect} =~ m/(Weave|Noise Paint)/)) ) {
	my $button = $oldparams{$plugin}{$effect}{sharpen_freq}{num};
	my $sharpen_f = $setup->{buttons}[$button]{val};
	$sharpen_f = 1.0 / $sharpen_f;

	my $channel = $setup->{channels}[$button];
	if (defined($channel)) {
	    $channel =~ s/(\sValue)\s+(-?[0-9.]+)/"$1 ".(1.0 \/ $2)/ge;
	    $channel =~ s/(\s(Left|Right)Slope)\s+(-?[0-9.]+)/"$1 ".(1.0 \/ $3)/ge;
	}

	$button = $newparams{$plugin}{$effect}{sharpen_width}{num};
	$newsetup->{buttons}[$button]{val} = $sharpen_f;

	if (defined($channel)) {
	    $newsetup->{channels}[$button] = $channel;
	}
    }
    if ($plugin =~ m/S_Sharpen/) {
	my $button = $oldparams{$plugin}{$effect}{at_frequency}{num};
	my $s_f = $setup->{buttons}[$button]{val};
	$s_f = 1.0 / $s_f;

	my $channel = $setup->{channels}[$button];
	if (defined($channel)) {
	    $channel =~ s/(\sValue)\s+(-?[0-9.]+)/"$1 ".(1.0 \/ $2)/ge;
	    $channel =~ s/(\s(Left|Right)Slope)\s+(-?[0-9.]+)/"$1 ".(1.0 \/ $3)/ge;
	}

	$button = $newparams{$plugin}{$effect}{sharpen_width}{num};
	$newsetup->{buttons}[$button]{val} = $s_f;	
	
	if (defined($channel)) {
	    $newsetup->{channels}[$button] = $channel;
	}
    }

    # FilmDamage replaced inner_radius and outer_radius with vignt_radius and 
    # vignt_edge_soft
    if ($plugin eq 'S_FilmDamage') {
	my $button = $oldparams{$plugin}{$effect}{vignt_inner_rad}{num};
	my $inner_rad = $setup->{buttons}[$button]{val};
	$button = $oldparams{$plugin}{$effect}{vignt_outer_rad}{num};
	my $outer_rad = $setup->{buttons}[$button]{val};

	my $v_rad = ($outer_rad + $inner_rad) * 0.5;
	my $softness = 2.0 * ($v_rad - $inner_rad);

	$button = $newparams{$plugin}{$effect}{vignt_radius}{num};
	$newsetup->{buttons}[$button]{val} = $v_rad;	
	$button = $newparams{$plugin}{$effect}{vignt_edge_soft}{num};
	$newsetup->{buttons}[$button]{val} = $softness;		

	# clear frequency channel since we can't really updated it correctly
	$newsetup->{channels}[$button] = '';
    }
    # In BlurDirectionalMask: bias now gets scaled to match BlurDirectional like:
    # (bias - .5) * 2
    if ($plugin eq 'S_BlursMask') {
	if ($neweffects{$plugin}{$effect} =~ m/Blur Directional Mask/)	 {
	    my $button = $oldparams{$plugin}{$effect}{bias}{num};
	    my $bias = $setup->{buttons}[$button]{val};
	    $bias = 0.5 * $bias + 0.5;
	    $button = $newparams{$plugin}{$effect}{bias}{num};
	    $newsetup->{buttons}[$button]{val} = $bias;
	    fix_channel_range($newsetup, $button, 0.5, 0.5); 	    
	}
    }


    # BLUR_USE_PIXEL_ASPECT 
    {
	my $scale_y = 1.0;
	if ( ($current_frame_width == 720) and ($current_frame_height == 486)) {
	    $scale_y = 1.11111;
	}
	elsif ( ($current_frame_width == 720) and ($current_frame_height == 576) ) {
	    $scale_y = 0.9375;
	}
	elsif ( ($current_frame_width == 1920) and ($current_frame_height == 1036) ) {
	    $scale_y = 1.04348;
	}
	elsif ( ($current_frame_width == 1828) and ($current_frame_height == 1556) ) {
	    $scale_y = 0.5;
	}

 	my $button_name = 'NONE';
	if ($plugin =~ m/S_Blurs/)  {
	    if ($neweffects{$plugin}{$effect} eq 'Blur Channels') {
		$button_name = 'blur_y';
	    }
	    elsif ( ($neweffects{$plugin}{$effect} eq 'Blur') or
		    ($neweffects{$plugin}{$effect} =~ m/(Chroma|Monochrome|Blur Mask)/ ) ) {
		$button_name = 'blur_rel.Y';
	    }	
	}
	if ($plugin eq 'S_Dissolves') {
	    if ($neweffects{$plugin}{$effect} eq 'Dissolve Blur') {
		$button_name = 'blur_rel.Y';
	    }
	}
	if ($plugin eq 'S_ZBlurs') {
	    if ($neweffects{$plugin}{$effect} eq 'Z Blur') {
		$button_name = 'width_rel.Y';
	    }
	}
	# All Glows (S_Glow, S_GlowComp, S_GlowMask, and S_GlowMaskCmp
	if ($plugin =~ m/S_Glows/) {
	    # Glow, GlowRigs, GlowNoise, GlowEdge, GlowOrth, and GlowDarks
	    # ! GlowAura, GlowRainbow, or GlowDist
	    my $button = $oldparams{$plugin}{$effect}{'width_y'}{val};
	    if(defined($button)) {
		$button_name = 'width_y';
	    }
	}
	if ($plugin =~ m/S_SoftFocus/) {
	    $button_name = 'width_rel.Y';
	}

	if ($button_name ne 'NONE') {
	    my $blur_y_button = $oldparams{$plugin}{$effect}{$button_name}{num};
	    if (defined($blur_y_button) ) {
		my $rel_blur_y = $setup->{buttons}[$blur_y_button]{val};
		$blur_y_button = $newparams{$plugin}{$effect}{$button_name}{num};
		$newsetup->{buttons}[$blur_y_button]{val} = $rel_blur_y * $scale_y;
		fix_channel_range($newsetup, $blur_y_button, $scale_y, 0);
	    }

	}

	if ($plugin eq 'S_BandPass') {
	    # v4 didn't have a rel_y, but v5 does.
	    # set rel_y to the $scale_y so that the preset looks approximately like it used to
	    my $blur_y_button = $newparams{$plugin}{$effect}{'blur_rel.Y'}{num};
	    $newsetup->{buttons}[$blur_y_button]{val} = $scale_y;
	}
    }
}

# Returns 1 for success, 0 for error.
sub print_setup 
{
    my($setup, $outfile) = @_;
    # print Dumper($setup);
    if (!open(OUTFILE, ">$outfile")) {
	Warn "Can't open temp file $outfile: $!.  Skipping this file.\n";
	return 0;
    }
    print OUTFILE $setup->{preamble};
    for (my $button = 0; $button < @{$setup->{buttons}}; $button++) {
	my $setup_btype = defined($setup->{buttons}[$button]) ?
	    $setup->{buttons}[$button]{type} : "OFF";
	printf OUTFILE ("SPARK_%s %s\n",
			$setup_btype,
			defined($setup->{buttons}[$button]{val}) ?
			' '.$setup->{buttons}[$button]{val}.' ' : '',
			$button);
	print OUTFILE $setup->{channels}[$button] if defined($setup->{channels}[$button]);
    }
    print OUTFILE $setup->{postamble};
    close OUTFILE;
}



sub clamp
{
    my ($val, $minv, $maxv) = @_;
    return $val > $maxv ? $maxv : ($val < $minv ? $minv : $val);
}

sub pack_color
{
    my($r, $g, $b) = @_;
    return (int(clamp($r,0,1) * 255 + .5) << 24 |
	    int(clamp($g,0,1) * 255 + .5) << 16 |
	    int(clamp($b,0,1) * 255 + .5) << 8);
}

sub convert_batchfile
{
    my ($file, $backup) = @_;
    # These regexes support both old and new batch file formats
    return convert_batchfile_or_node($file, $backup,
				     'BatchFileVersion|BatchLength',
				     ['BatchFileVersion|BatchLength',
				      'CreationDate',
				      'BatchEnd|Setup',
				      'Spark']);
}
sub convert_nodefile
{
    my ($file, $backup) = @_;
    return convert_batchfile_or_node($file, $backup,
				     'SparkDSO',
				     ['Setup',
				      'SparkDSO',
				      'SparkProduct',
				      "$olddir"]);
}

# Try to convert the batch file given in the first arg.
# Return 1 on success or 0 on any error.
# This works on a .batch file (either pre-2008, with Sapphire nodes or
# post-2008, without nodes), and/or on a spark_node file containing
# one spark in XML format (we ignore the XML and just use regexes as usual.)
sub convert_batchfile_or_node
{
    # $first_check is a regex to check for that should be present in
    # the first 100 lines.  Sanity check that we're really looking at
    # a batch file.
    # $check_all is an array of regexes, *all* of which must match,
    # otherwise the file is rejected as not being a valid batch file.
    my ($file, $backup, $first_check, $check_all) = @_;
    my ($batchfile, $did_backup) = ("", 0);
    # Check to make sure it is a batch file

    print "Converting batch/node file $file (maybe; test=$first_check)...\n" if $debug;
    
    if ($file =~ /\.batch\.v$oldversion/) {
	Warn "already a Sapphire v$oldversion batch backup.\n";
	return 0;
    }
    if (!open (INFILE, $file)) {
	Warn "Can't read $file: $!\n";
	return 0;
    }
    while (<INFILE>) {
	$batchfile .= $_;	# slurp it into one big string, I'm lazy.
	# But if it gets *too* big (i.e. BatchFileVersion not found in
	# first 100 lines), just punt.  It must not be a batch file.
	return 0 if ($. == 100 && $batchfile !~ /BatchFileVersion/)
	}

    foreach my $re (@$check_all) {
	if ($batchfile !~ /$re/) {
	    print "$file is missing \"$re\", not a batch file (this is OK).\n" if $debug;
	    return 0;
	}
    }

    if ($backup_p) {
	if (-f $backup) {
	    Print "backup file already exists; I'll leave it.\n";
	} elsif (move($file, $backup)) {
	    $did_backup = 1;
	} else {
	    Warn "Can't rename $file to $backup: $!.  Skipping conversion.\n";
	    return 0;
	}
    }
    else {
	# No backup desired: try to remove the file before rewriting
	# it.  If that doesn't work, try renaming it to backup anyway.
	if (!unlink $file) {
	    if (move($file, $backup)) {
		Print "couldn't remove file; ".
		    "moved it to $backup before converting.\n";
		$did_backup = 1;
	    }
	    else {
		Warn "Can't remove or rename original: $!. ".
		    "(skipping conversion)\n";
		return 0;
	    }
	}
    }
    # Convert all references to old dir to new dir
    $batchfile =~ s{(SparkDSO.*)/$olddir/S_}{$1/$newdir/S_}g;
    $batchfile =~ s{SparkProduct $olddir}{SparkProduct $newdir}g;
    # Rename plugins if needed
    for my $old ( keys %renames) {
	my $new = $renames{$old};
	$batchfile =~ s{(SparkDSO.*)/$old\.(spark(_x86_64|_64)?)}{$1/$new.$2}g;
	$batchfile =~ s{SparkExt $old$}{SparkExt $new}m;
    }

    # XXX: other special cases in the batch file itself (adding new
    # inputs etc.) go here

    if (!(open OUTFILE, ">$file")) {
	Warn "Can't write to $file: $!\n";
	move ($backup, $file) if $did_backup;
	return 0;
    }
    print OUTFILE $batchfile;
    close OUTFILE;
    return 1;
}

# Process one file or directory name
sub do_file_or_dir
{
    my ($filename, $warn_if_nonexistent) = @_;
    my $renamed_p = 0;

    # Set globals for message printing
    $current_file = $filename;
    $file_name_printed = 0;

#    print "do_file_or_dir: $filename\n" if ($debug);

    if ($warn_if_nonexistent && ! -d $filename && ! -f $filename) {
	Warn "ERROR: No such file or dir $filename.\n";
	return;
    }
    if (-d $filename && $recurse_p) {
	# Remove trailing / from directory names
	$filename =~ s/\/$//;

	# XXX: what is this stanza for, really?
	if ($filename =~ m,(^|/)sapphire_[23]\.0$,) {
	    my $cwd = Cwd::cwd();
	    chdir $filename;
	    my $realname = Cwd::cwd();
	    chdir $cwd;
	    if ($filename ne "/usr/discreet/sparks/$olddir" &&
		$realname ne "/usr/discreet/sparks/$olddir") {
		(my $newname = $filename) =~ s/$olddir$/$newdir/;
		if (!rename($filename, $newname)) {
		    Warn "Can't rename dir to $newdir; $!.  " .
			"Please do it manually.  (Continuing anyway.)\n";
		}
		else {
		    $filename = $newname;
		    Print "renamed to $newname.\n";
		}
	    }
	    else {
		Warn "Not renaming sapphire install dir.\n";
	    }
	}

	for my $old ( keys %renames) {
	    my $new = $renames{$old};
	    
	    if ($filename =~ m,(^|/)$old$,) {
		my $newname = $filename;
		# We need to rename dirs when the plugin name has changed
		$newname =~ s/$old$/$new/;
		if (!rename($filename, $newname)) {
		    Warn "Can't rename dir $filename to $newname; $!.  " .
			"Please do it manually.  (Continuing anyway.)\n";
		}
		else {
		    $filename = $newname;
		    Print "dir renamed to $newname.\n";
		}
	    }
	}
	opendir DIR, $filename;
	my (@filenames) = grep { !/^\./ } readdir DIR;
	my (@dirs) = grep { -d "$filename/$_" } @filenames;
	my (@files) = grep { -f "$filename/$_" } @filenames;
	closedir DIR;
# This is too verbose: many dirs with no Sapphire stuff in them have
# no write permission.
#	if (! -w $filename && @files >= 0) {
#	    Warn "Skipping all files in this dir; no write permission.\n";
#	    @files = ();
#	}
	foreach my $fname (sort(@files), sort(@dirs)) {
	    do_file_or_dir($filename . "/" . $fname, 0);
	}
	return;
    }
    
    # Rename proxies as well, why not.
    for my $old ( keys %renames) {
	my $new = $renames{$old};
	if (-f $filename && $filename =~ m/\.$old\.p$/) {
	    (my $newname = $filename) =~ s/$old\.p$/$new.p/;
	    if (!rename($filename, $newname)) {
		Warn "Can't rename proxy to $newname; $!.  " .
		    "Please do it manually.  (Continuing anyway.)\n";
	    }
	    else {
		$filename = $newname;
		Print "proxy renamed to $newname.\n";
	    }
	    
	    return;
	}
    }

    # Ignore all other non-file arguments (silently)
    # This will ignore all sockets, symlinks, and so on.
    return if (! -f $filename);

    # Now process the file; should be a .batch, setup, or .spark_node.

    my $bak_file = $filename;
    my $tmp_file = get_tmpfile();
    if ($bak_file !~ s/\.(S_[-_A-Za-z0-9]+)$/.v$oldversion.$1/ &&
	$bak_file !~ s/\.batch$/.batch.v$oldversion/) {
	$bak_file = $filename.".v$oldversion"; # shouldn't happen, just in case
    }
    if ($filename =~ /\.v$oldversion\.S_[-_A-Za-z0-9]+$/) {
	Print "already a Sapphire $oldversion backup; skipping.\n";
	return;
    }
    my %setup = ();
    my %newsetup = ();
    if (parse_setup($filename, \%setup)) {
	if ($setup{preamble} !~ m{SparkFileVersion}) {
	    # not a setup; skip silently
	    return;
	}
	# Look for Sapphire directory (old version) in preamble. Bail out if not found.
	if ($setup{preamble} !~ m{/$olddir/}) {
	    if ($setup{preamble} =~ m{/$newdir/}) {
		Print "already a Sapphire $newversion setup; skipping.\n";
	    }
	    elsif ($setup{preamble} =~ m{/$ignoredir/}) {
		Print "incompatible Sapphire v$ignoreversion setup; can't convert (skipping).\n";
	    }
	    else {
		Print "not a Sapphire setup; skipping.\n";
	    }
	    return;
	}

	my $do_backup = $backup_p;
	# change the file name from $olddir to $newdir if needed
	$filename =~ m,^((.*)/)?([^/]*)$,;	# get dir and file parts
	my ($dir,$file) = ($2,$3);
	my $realfile = $file;
	$file =~ s/\.$olddir\./.$newdir./g;

	for my $old ( keys %renames) {
	    my $new = $renames{$old};
	    $file =~ s/\.$old$/.$new/g;
	}
	$renamed_p = 1 if ($file ne $realfile);
	my $new_filename = defined($dir) ? $dir . "/" . $file
	    : $file;

	if ($backup_p && -f $bak_file) {
	    # Backup already exists; do nothing.
	    Warn "Backup file $bak_file already exists; I'll leave it.\n";
	    $do_backup = 0;
	}
	process_setup(\%setup, \%newsetup);
	print_setup(\%newsetup, $tmp_file) || return 0;
	if ($do_backup) {
	    if (!move($filename, $bak_file)) {
		Warn "Can't back up to $bak_file; " .
		    "skipping conversion ($!).\n";
		return 0;
	    }
	}
	else {
	    if (!unlink($filename) && !move($filename, $bak_file)) {
		Warn "Can't remove or back up original: $!. " .
		    "(skipping conversion)\n";
		return 0;
	    }
	}
	# Now original should no longer exist, so this move should
	# work.  Note: must use move because this could cross
	# filesystem boundary, so rename wouldn't work.
	if (!move($tmp_file, $new_filename)) {
	    Warn "Can't move temp file to $filename: $!; ".
		"skipping conversion.\n";
	    return 0;
	}
	$n_converted++;
	if ($renamed_p) {
	    Print "converted and renamed to $new_filename.\n";
	}
	else {
	    Print "converted.\n";
	}
    }
    elsif (convert_batchfile($filename, $bak_file)) {
	Print "batch file converted.\n";
	$n_converted++;
    }
    elsif (convert_nodefile($filename, $bak_file)) {
	Print "spark node file converted.\n";
	$n_converted++;
    }
    else {
	# do nothing (this is normal)
    }
}

# Main code starts here

use Getopt::Long;
GetOptions("recurse" => \$recurse_p,
	   "backup" => \$backup_p,
	   "print-cmds" => \$print_commands,
	   "show-commands" => \$print_commands,
	   "debug" => \$debug);

if (@ARGV < 1) {
    print "This utility updates Sapphire setup and batch files from version $oldversion to 
$newversion. It will convert and rewrite each Sapphire setup file you specify to
Sapphire $newversion format.\n";
    print "Usage: $0 [-recurse] [-backup] setup-file ...\n";
    exit(1);
}


# Find the param database files:
my $dbase_dir;
my $execdir;
($execdir = $0) =~ s|/[^/]*$||; # strip off last component

foreach my $dir ('.', $execdir,
		 '/usr/discreet/sparks/$newdir') {
    ($dbase_dir = $dir, last) if (-e "$dir/old-params.txt");
}

if (!defined($dbase_dir)) {
    my $cwd = Cwd::cwd();
    die "ERROR: Can't find my config files old-params.txt or new-params.txt.  I looked in
$cwd, $execdir, and /usr/discreet/sparks/$newdir, and couldn't find them.
Perhaps you have an installation problem?\n";
}

print "Initializing...\n";
read_param_files("$dbase_dir/old-params.txt", "$dbase_dir/new-params.txt");
compute_change_cmds();

if ($print_commands) {
    print "================ CHANGE COMMANDS ===========================\n";
    print_cmds();
    print "================ END CHANGE COMMANDS ===========================\n";
}

FILE:
    foreach my $name (@ARGV) {
	do_file_or_dir($name, 1);
    }

print "Done.  ",$n_converted," files updated.\n";
exit 0;
# eof
