#!/usr/local/bin/perl -w

use strict;
use File::Basename;
use FileHandle;
use Getopt::Long;
use POSIX 'sys_wait_h';

use remedyExport;
BEGIN {
  require 'ar.ph';
}
use ARS;	# ARSperl API
#exp use lib "$root/cgi-bin";
#exp use DumpVar;
#exp use Proc;
#exp use String;
#exp use Variable;
#exp no lib "$root/cgi-bin";
#exp use lib "$root/private/cgi-bin";
#exp use ARSesi;
#exp no lib "$root/private/cgi-bin";

STDOUT->autoflush( 1 );

# global variables
  
my $basename = basename( $0 );
my $delBad = 0;
my %dTypes = (
  def  => undef,
  dump => undef,
);
my $interrupted = 0;
my @objectRE;
my $write = 1;
my %specialProcessing = (
  helpText       => \&splitMultiline,
  macroText      => \&splitMultiline,
  messageText    => \&splitMultiline,
  sqlCommand     => \&splitMultiline,
  timestamp      => \&addCalendarTimestamp,
  TmHourMask     => \&convertMask,
  TmMonthDayMask => \&convertMonthDayMask,
  TmWeekDayMask  => \&convertWeekDayMask,
);

# base info source

my $existing = 1;

# store all of the prop info

my %prop = &parseProp();

{
  my $ctrl;
  my $loopInterval;

  my %types = (
    activelink => {
      all => {
        table => 'actlink',
        name  => 'name',
        id    => 'actlinkId',
      },
      one => {
        code => \&ars_GetActiveLink,
      },
      export  => 'active_link',
      prop     => {
        list => {
          &AR_OPROP_SCC_LOCKED_BY                         => undef,
          &AR_OPROP_SCC_TIMESTAMP                         => undef,
          &AR_OPROP_SCC_USER                              => undef,
          &AR_OPROP_SCC_LOCATION                          => undef,
          &AR_OPROP_WINDOW_OPEN_IF_SAMPLE_SERVER_SCHEMA   => undef,
          &AR_OPROP_WINDOW_OPEN_ELSE_SAMPLE_SERVER_SCHEMA => undef,
        },
        loc  => '{objPropList}',
      },
      text => 'active link',
    },
   guideActivelink => {
      all => {
        table => 'arcontainer',
        name  => 'name',
        id    => 'containerId',
        where => 'containerType = '. &ARS::ARCON_GUIDE(),
      },
      one => {
        code => undef,
      },
      export  => 'container',
      prop    => {
        list => {
        },
        loc  => '{xyz}',
      },
      text => 'active link guide',
    },
    application => {
      all => {
        table => 'arcontainer',
        name  => 'name',
        id    => 'containerId',
        where => 'containerType = '. &ARS::ARCON_APP(),
      },
      one => {
        code => undef,
      },
      export  => 'container',
      prop    => {
        list => {
        },
        loc  => '{xyz}',
      },
      text => 'application',
    },
    charmenu   => {
      all => {
        table => 'char_menu',
        name  => 'name',
        id    => 'charMenuId',
      },
      one => {
        code => \&ars_GetCharMenu,
      },
      export  => 'char_menu',
      text => 'char menu',
    },
    escalation => {
      all => {
        table => 'escalation',
        name  => 'name',
        id    => 'escalationId',
      },
      one => {
        code => \&ars_GetEscalation,
      },
      export  => 'escalation',
      prop     => {
        list => {
        },
        loc  => '{objPropList}',
      },
      text => 'escalation',
    },
    filter     => {
      all => {
        table => 'filter',
        name  => 'name',
        id    => 'filterId',
      },
      one => {
        code => \&ars_GetFilter,
      },
      export  => 'filter',
      prop     => {
        list => {
        },
        loc  => '{objPropList}',
      },
      text => 'filter',
    },
    guideFilter => {
      all => {
        table => 'arcontainer',
        name  => 'name',
        id    => 'containerId',
        where => 'containerType = '. &ARS::ARCON_FILTER_GUIDE(),
      },
      one => {
        code => undef,
      },
      export  => 'container',
      prop    => {
        list => {
        },
        loc  => '{xyz}',
      },
      text => 'filter guide',
    },
    packingList => {
      all => {
        table => 'arcontainer',
        name  => 'name',
        id    => 'containerId',
        where => 'containerType = '. &ARS::ARCON_PACK(),
      },
      one => {
        code => undef,
      },
      export  => 'container',
      prop    => {
        list => {
        },
        loc  => '{xyz}',
      },
      text => 'packing list',
    },
    schema     => {
      all => {
        table => 'arschema',
        name  => 'name',
        id    => 'schemaId',
      },
      one => {
        code => \&ars_GetSchema,
      },
      export  => 'schema',
      prop     => {
        list => {
        },
        loc  => '{objPropList}',
      },
      text => 'schema',
      dep  => {
        field => {
          all => {
            table => 'field',
            name  => 'fieldName',
            id    => 'fieldId',
          },
          one => {
            code => \&ars_GetField,
            params => '$parent, $id',
          },
          prop     => {
            list => {
            },
            loc  => '{displayInstanceList}{dInstanceList}[]{props}',
          },
          text => 'field',
        },
        vui   => {
          all => {
            table => 'vui',
            name  => 'vuiName',
            id    => 'vuiId',
          },
          one => {
            code => \&ars_GetVUI,
            params => '$parent, $id',
          },
          prop     => {
            list => {
              &AR_DPROP_DETAIL_PANE_IMAGE => undef,
              &AR_DPROP_HTML_TEXT         => undef,
            },
            loc  => '{props}',
          },
          text => 'vui',
        },
      },
    },
    webservice => {
      all => {
        table => 'arcontainer',
        name  => 'name',
        id    => 'containerId',
        where => 'containerType = '. &ARS::ARCON_WEBSERVICE(),
      },
      one => {
        code => undef,
      },
      export  => 'container',
      prop    => {
        list => {
        },
        loc  => '{xyz}',
      },
      text => 'web service',
    },
  );

  # work through the command line options

  my $rootDir;
  my $updating;
  {
    my @args = @ARGV;
    my $arTcpPort;
    my $pass;
    my $server;
    my $user;
    my %use;
    my %useList = (
      'ARTCPPORT=i'     => \$arTcpPort,
      'delBad!'         => \$delBad,
      'existing!'       => \$existing,
      'loopInterval=i'  => \$loopInterval,
      'objectRE=s@'     => \@objectRE,
      'updating!'       => \$updating,
      'write!'          => \$write,
    );
    foreach( keys( %types )) {
      $use{$_} = undef;
      $useList{$_. '!'} = \$use{$_};
    }
    foreach( keys( %dTypes )) {
      $useList{$_. '!'} = \$dTypes{$_};
    }
    my %req = (
     'rootDirectory=s' => \$rootDir,
     'server=s'        => \$server,
     'username=s'      => \$user,
     'password=s'      => \$pass,
    );

    if( !&GetOptions( %req, %useList )) {
      print STDERR "Error reading command line arguments\n";
      &showUsage( keys( %req ), "\0", keys( %useList ), "\0", "\0",
                  "'objectRE' uses UNIX wildcards (* and ?)" );
    }

    # there should be no arguments left

    if( @ARGV ) {
      print STDERR "Additional command line parameters found: @ARGV\n";
      &showUsage( keys( %req ), "\0", keys( %useList ), "\0", "\0",
                  "'objectRE' uses UNIX wildcards (* and ?)" );
    }

    # verify all required parameters have been specified

    my @error;
    foreach( keys( %req )) {
      my $p = $req{$_};
      if(( ref( $p ) eq 'ARRAY' ) ? !@{$p} : !defined( ${$p} )) {
        s/[=:!].*//;
        push( @error, $_ );
      }
    }
    if( @error ) {
      foreach( sort( @error )) {
        print STDERR "Missing required parameter: $_\n";
      }
      &showUsage( keys( %req ), "\0", keys( %useList ), "\0", "\0",
                  "'objectRE' uses UNIX wildcards (* and ?)" );
    }

    # die if the root directory does not exist

    if( !-e $rootDir ) {
      die "Root directory '$rootDir' does not exist\n";
    }

    # write out the call

    chomp( my $date = `date "+%Y/%m/%d %H:%M:%S"` );
    if( !$updating ) {
      print "$date: '", join( "' '", $0, @args ), "'\n";
    }

    # remove undesired input types

    my $found = 0;
    foreach( keys( %use )) {
      if( defined( $use{$_} )) {
        if( !$use{$_} ) {
          delete( $use{$_} );
          delete( $types{$_} );
        }
        else {
          $found = 1;
        }
      }
    }
    if( $found ) {

      # at least one specified to include, so remove the unincluded types
  
      foreach( keys( %use )) {
        if( !defined( $use{$_} )) {
          delete( $use{$_} );
          delete( $types{$_} );
        }
      }
    }
    if( !keys( %types )) {
      die "No object types to process\n";
    }
  
    # set processing routines for each dump type requested

    undef $found;
    foreach( keys( %dTypes )) {
      if( defined( $dTypes{$_} )) {
        $found = $_;
        last;
      }
    }
    if( defined( $found )) {
      foreach( keys( %dTypes )) {
        if( !defined( $dTypes{$_} )) {
          $dTypes{$_} = !$dTypes{$found};
        }
      }
    }
    else {
      foreach( keys( %dTypes )) {
        $dTypes{$_} = 1;
      }
    }
    foreach( keys( %dTypes )) {
      if( $dTypes{$_} ) {
        my $cmd = '$dTypes{'. $_. '} = \&process'. uc( $_ ). 'data';
        eval( $cmd );
        if( $@ ) {
          die "Error eval'ing '$cmd':\n",
               $@;
        }
      }
    }

    # set up the ARTCPPORT environment variable if defined

    if( defined( $arTcpPort )) {
      $ENV{ARTCPPORT} = $arTcpPort;
    }
  
    # login to Remedy no so we don't have to push around the login info

    if( !defined( $ctrl )) {
      $ctrl = &ars_Login( $server, $user, $pass );
      if( !defined( $ctrl )) {
        die "Unable to connect to Remedy server '$server'\n";
      }
    }
  }

  for(;; ) {

    # process each desired type
  
    foreach my $type( sort( keys( %types ))) {
  
      # process all objects of the type
  
      &processType( $ctrl, $type, $types{$type}, \%dTypes, $rootDir, \@objectRE,
                    $write );
    }
  
    if( !$updating ) {
      chomp( my $date = `date "+%Y/%m/%d %H:%M:%S"` );
      print "$date: $0 completed\n";
    }

    # determine to loop again

    if( !defined( $loopInterval )) {
      last;
    }
    else {
      my $oldINT = $SIG{INT};
      $SIG{INT} = sub { print "\n"; exit; };
      print "\n",
            "Sleeping for $loopInterval second", ( $loopInterval == 1
                                                  ? '' : 's' ), " ...";
      sleep( $loopInterval );
      $SIG{INT} = defined( $oldINT ? $oldINT : undef );
      print " Woke up\n\n";
    }
  }

  exit;
}

sub addCalendarTimestamp {

  my( $second, $minute, $hour, $day, $month, $year ) = localtime( $_[1] );
  my %ret = ( actual => $_[1],
              visual => sprintf( "%4.4d/%2.2d/%2.2d %2.2d:%2.2d:%2.2d",
                                 $year + 1900, $month + 1, $day, $hour, $minute, $second ));
  return( \%ret );
}

sub convertMask {
  my @ret;
  for( my $p = 0; $_[1]; $p++ ) {
    if( $_[1] % 2 ) {
      push( @ret, $p );
    }
    $_[1] >>= 1;
  }
  return( join( ',', @ret ));
}

sub convertMonthDayMask {
  my $hex = sprintf( '%x', $_[1] );
  my @ret;
  for( my $p = 1; $_[1]; $p++ ) {
    if( $_[1] % 2 ) {
      push( @ret, $p );
    }
    $_[1] >>= 1;
  }
  return([ $hex, join( ',', @ret )]);
}

sub convertWeekDayMask {
  my @days = ( 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' );
  my @ret;
  for( my $p = 0; ( $_[1] ) && ( $p < @days ); $p++ ) {
    if( $_[1] % 2 ) {
      push( @ret, $days[$p] );
    }
    $_[1] >>= 1;
  }
  return( join( ',', @ret ));
}

sub convertToFilename {
  ( my $ret = shift ) =~ s/[^-\w+_!*?]/_/g;
  return( $ret );
}

sub moveFile {
  my( $write, $timestamp, $real, $new ) = @_;

  # move the file

  if( $write ) {
    my $errstr = `/bin/mv $real $new 2>&1`;
    if( $? ) {
      die "Error executing '/bin/mv $real $new' ($?):\n",
          $errstr;
    }
  }
  else {
    print "/bin/mv $real $new\n";
  }

  # touch the file with the given timestamp

  &touchFile( $write, $new, $timestamp );
  print "\nMoved $real to $new\n";
}

sub parseProp {

  chomp( my @file = `find @INC -name ar.ph 2>/dev/null` );

  my %ret;
  if( !open( AR, "<$file[0]" )) {
    die "Error opening '$file[0]' for read access\n";
  }
  foreach( grep( /eval 'sub AR_.*PROP.*\(\) \{\d+;\}/, <AR> )) {
    my( $prefix, $prop, $val ) = /eval 'sub AR_(.+)PROP_(.+) \(\) \{(\d+);\}/;
    $ret{$val} = $prop;
  }
  return( %ret );
}

sub processType {
  my( $ctrl, $type, $p_typeinfo, $p_dTypes, $rootDir, $p_objectRE, $write,
      $where, $parent ) = @_;
  my %typeinfo = %{$p_typeinfo};
  my %dTypes = %{$p_dTypes};

  # read the type's control info and store

  if( !defined( $parent )) {
    print "Reading ", ( defined( $parent )
                       ? "'$parent' "
                       : '' ), "$typeinfo{text} records";
  }

  my @where;
  if( defined( $where )) {
    push( @where, '('. $where. ')' );
  }
  if( defined( $typeinfo{all}{where} )) {
    push( @where, '('. $typeinfo{all}{where}. ')' );
  }
  if( @{$p_objectRE} ) {
    my @where2;
    foreach( @{$p_objectRE} ) {
      ( my $re = $_ ) =~ s/\*/%/g;
      $re =~ s/\?/_/g;
      $re =~ s/'/''/g;
      push( @where2, "($typeinfo{all}{name} LIKE '$re')" );
    }
    push( @where, '('. join( ' OR ', @where2 ). ')' );
  }
    
  if( !defined( $typeinfo{all}{id} )) {
    print STDERR "Type '$type' has no 'id' field specified:\n";
    dumpVar( $type, \%typeinfo );
    die;
  }
  my $query = "SELECT $typeinfo{all}{name},timestamp".
               ( defined( $typeinfo{all}{id} ) ? ",$typeinfo{all}{id}" : '' ).
               " FROM $typeinfo{all}{table}".
               ( @where
                ? " WHERE ". join( ' AND ', @where )
                : '' );
  my $p_data =
   &ars_GetListSQL( $ctrl, $query, 0 );
  if( defined( $ars_errstr )) {
    print STDERR "\n",
                 "   Error reading $typeinfo{text} list:\n",
                 "    $ars_errstr\n";
    die "$basename aborted\n";
  }
  
  my %list;
  my %id;
  foreach( @{${$p_data}{rows}} ) {
    $id{${$_}[2]} = { object    => ${$_}[0] };
    foreach my $dType( keys( %dTypes )) {
      $id{${$_}[2]}{timestamp}{$dType} = ${$_}[1];
    }
    $list{${$_}[0]} = ${$_}[2];
  }
  if( !defined( $parent )) {
    print " (", scalar( keys( %list )), " entr", ( keys( %list ) == 1
                                                  ? 'y'
                                                  : 'ies' ), ")\n";
  }

  if(( !defined( $parent )) && ( $existing )) {
    my $types = '('. join( '|', keys( %dTypes )). ')';

    # loop through all desired objects

    my $findCMD = "find $rootDir/current/*/$type -type f";
    my %fileList;
    if( @{$p_objectRE} ) {
      foreach( @{$p_objectRE} ) {

        # convert UNIX wildcards to Perl and adjust filename

        my $filename = &convertToFilename( $_ );
        foreach( `$findCMD -name '$filename\.*' 2>/dev/null` ) {
          chomp;
          $fileList{$_} = 1;
        }
      }

      # loop through all found IDs

      foreach( keys( %id )) {
        foreach( `$findCMD -name '*.$_.*' 2>/dev/null` ) {
          chomp;
          $fileList{$_} = 1;
        }
      }
    }
    else {
      foreach( `$findCMD 2>/dev/null` ) {
        chomp;
        $fileList{$_} = 1;
      }
    }

    if( keys( %fileList )) {

      # set up an RE to grab the important info

      my $format = '.*\.(\d+)\.\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.'. $types;

      my $total = keys( %fileList );
      my $cnt = 0;
      print "Comparing ", ( defined( $parent )
                           ? "'$parent' "
                           : '' ), "$typeinfo{text} file";
      my $pct = 0;
      foreach( keys( %fileList )) {
        use integer;
        if((( ++$cnt * 100 ) / $total ) > $pct ) {
          $pct = ( $cnt * 100 ) / $total;
          print "\rComparing ", ( defined( $parent )
                                 ? "'$parent' "
                                 : '' ),
                "$typeinfo{text} file $cnt of $total, $pct%";
        }
        no integer;
        ( my $id, my $dType ) = ( /$format$/ );
        if( !defined( $id )) {
          if( $delBad ) {
            print `rm -f $_`;
            next;
          }
          die "\nInvalid filename found: '$_'\n";
        }
        my( $object, $timestamp ) = &getData( $_, $delBad );
        if( !defined( $object )) {
          next;
        }

        # if the id doesn't exist, it's been deleted from Remedy, so move it

        if( !exists( $id{$id} )) {
          &moveOld( $write, $rootDir, $dType, $type, $id,
                    { file      => $_,
                      timestamp => $timestamp } );
        }

        elsif( !exists( $id{$id}{timestamp}{$dType} )) {
          die "\nMultiple $typeinfo{text} $dType records for id $id\n";
        }

        # move the file if the object is not current

        elsif( $id{$id}{timestamp}{$dType} != $timestamp ) {
          $id{$id}{move}{$dType} = { file      => $_,
                                     timestamp => $timestamp };;
        }

        # the timestamps match so no need to process

        else {
          delete( $id{$id}{timestamp}{$dType} );
        }
      }
      print "\rComparing ", ( defined( $parent )
                             ? "'$parent' "
                             : '' ),
            "$typeinfo{text} file $cnt of $total, complete\n";
    }
  }

  # remove any ids which are not going to be processed

  foreach my $id( keys( %id )) {
    if( !keys( %{$id{$id}{timestamp}} )) {
      delete( $id{$id} );
    }
  }

  # remove any unused dump types

  foreach( keys( %dTypes )) {
    if( ref( $dTypes{$_} ) ne 'CODE' ) {
      delete( $dTypes{$_} );
    }
  }

  my %total;
  @total{keys( %dTypes )} = ('0') x keys( %dTypes );

  # get counts for each dump type and delete if none

  foreach my $id( keys( %id )) {
    foreach my $dType( keys( %dTypes )) {
      $total{$dType}++ if( exists( $id{$id}{timestamp}{$dType} ));
    }
  }

  # remove the dump dType if typeinfo{all}{code} is not defined and there are
  #  items to process

  if(( exists( $dTypes{dump} )) && ( !defined( $typeinfo{one}{code} )) &&
     ( $total{dump} )) {
    print "Type '$type' can't create a dump file\n";
    delete( $dTypes{dump} );
    $total{dump} = 0;
  }

  my $total = 0;
  foreach( keys( %dTypes )) {
    if( !$total{$_} ) {
      delete( $dTypes{$_} );
    }
    $total += $total{$_};
  }

  # process all objects of the type

  my %object;
  foreach( keys( %id )) {
    $object{$id{$_}{object}} = $_;
  }
  my %ret;
  my $cnt = 0;
  ID:
  foreach my $object( sort( keys( %object ))) {
    foreach my $dType( sort( keys( %dTypes ))) {

    # process the output type

      if( exists( $id{$object{$object}}{timestamp}{$dType} )) {
        if( $write ) {
          my %dType = ( $dType => $dTypes{$dType} );
          $cnt++;
          $ret{$object} =
           &{$dTypes{$dType}}( $ctrl, $cnt, $total, $object, $object{$object},
                               $id{$object{$object}}{move}{$dType}, $type,
                               $p_typeinfo, \%dType, $rootDir, $where, $parent );
          if( !defined( $ret{$object} )) {
            last ID;
          }
        }
        else {
          print "# write $typeinfo{text} '$object'\n";
        }
      }
    }
  }

  return( \%ret );
}

sub processDEFdata {
  my( $ctrl, $cnt, $total, $object, $id, $oldObject, $type, $p_typeinfo,
      $p_dTypes, $rootDir, $where, $parent ) = @_;

  my $def = &ars_Export( $ctrl, '', &ARS::AR_VUI_TYPE_NONE,
                         ${$p_typeinfo}{export}, $object );
  if( defined( $ars_errstr )) {
    print STDERR " Error reading ${$p_typeinfo}{text} '$object':\n",
                 "  $ars_errstr\n";
    die "$basename aborted\n";
  }

  my $filename = &convertToFilename( $object );
  $filename .= ".$id";

  # move the file from enabled or disabled (opposite or current) if exists in the
  #  other directory

  if( defined( $oldObject )) {
    &moveOld( $write, $rootDir, 'def', $type, $id, $oldObject );
  }

  # write the data

  my $ena;
  if( $def =~ /\n   enable         : (\d+)\n/s ) {
    $ena = $1;
  }

  my $dir = $rootDir. '/current/def/'. $type. ( defined( $ena )
                                               ? '/'. ( $ena
                                                       ? 'enabled'
                                                       : 'disabled' )
                                               : '' );
  my $errstr = `mkdir -p $dir 2>&1`;
  if( $? ) {
    print STDERR "Error executing 'mkdir -p $dir' ($?):\n",
                 $errstr;
    return( undef );
  }

  # write the new file

  print "Writing ${$p_typeinfo}{text} $cnt of $total ($object,def)\n";

  # append the visual timestamp to the filename

  ( my $timestamp ) = ( $def =~ /.*\n   timestamp      : (\d+)\n.*/m );
  {
    my( $second, $minute, $hour, $day, $month, $year ) = localtime( $timestamp );
    $filename .= sprintf( ".%4.4d-%2.2d-%2.2d_%2.2d-%2.2d-%2.2d",
                          $year + 1900, $month + 1, $day, $hour, $minute,
                          $second );
  }
  
  if( !open( OUT, ">$dir/$filename.def" )) {
    print STDERR "Error opening ${$p_typeinfo}{text} '$dir/$filename.def' ",
                 "for write access\n";
    return( undef );
  }

  print OUT "# NAME: $object\n",
            "# TIMESTAMP: $timestamp\n",
            $def;
  close( OUT );

  &touchFile( $write, "$dir/$filename.def", $timestamp );
  return( \$def );
}

sub moveOld {
  my( $write, $rootDir, $dType, $type, $id, $p_old ) = @_;

  if( !defined( $p_old )) {
    return;
  }

  my $cmd = "mkdir -p $rootDir/previous/$dType/$type/";
  if( $write ) {
    my $errstr = `$cmd 2>&1`;
    if( $? ) {
      die "Error executing '$cmd' ($?):\n",
          $errstr;
    }
  }
  else {
    print "$cmd\n";
  }
  my $filename = basename( ${$p_old}{file} );
  &moveFile( $write, ${$p_old}{timestamp}, ${$p_old}{file},
             "$rootDir/previous/$dType/$type/$filename" );
  return;
}

sub processDUMPdata {
  my( $ctrl, $cnt, $total, $object, $id, $oldObject, $type, $p_typeinfo,
      $p_dTypes, $rootDir, $where, $parent ) = @_;
  my %typeinfo = %{$p_typeinfo};

  my @params = $object;
  if( defined( $typeinfo{one}{params} )) {
    eval( '@params = ('. $typeinfo{one}{params}. ')' );
    if( $@ ) {
      die "Error eval'ing '\@params = ( $typeinfo{soloParams} )':\n",
           $@;
    }
  }
  my $obj = &{$typeinfo{one}{code}}( $ctrl, @params );
  if( defined( $ars_errstr )) {
    print STDERR " Error reading $typeinfo{text} '$object':\n",
                 "  $ars_errstr\n";
    die "$basename aborted\n";
  }

  # add name if not present

  if( !exists( ${$obj}{$typeinfo{all}{name}} )) {
    ${$obj}{$typeinfo{all}{name}} = $object;
  }

  # process property list

  if( exists( $typeinfo{prop} )) {
    &processProp( $typeinfo{text}, $typeinfo{prop}{list}, $obj,
                  $typeinfo{prop}{loc} );
  }

  # process any dependents

  if( exists( $typeinfo{dep} )) {
    my $where = "$typeinfo{all}{id} = $id";
    foreach my $dep( keys( %{$typeinfo{dep}} )) {
      ${$obj}{$dep} = &processType( $ctrl, $dep, $typeinfo{dep}{$dep}, $p_dTypes,
                                    $rootDir, [], $write, $where, $object );
    }
  }

  # write the data if parent is not defined

  if( !defined( $parent )) {
    my $filename = &convertToFilename( $object );
    $filename .= ".$id";
  
    # move the file from enabled or disabled (opposite or current) if exists in
    #  the other directory

    if( defined( $oldObject )) {
      &moveOld( $write, $rootDir, 'dump', $type, $id, $oldObject );
    }

    # write the data
  
    my $ena;
    if( exists( ${$obj}{enable} )) {
      $ena = ${$obj}{enable};
    }
  
    my $dir = $rootDir. '/current/dump/'. $type. ( defined( $ena )
                                                  ? '/'. ( $ena
                                                          ? 'enabled'
                                                          : 'disabled' )
                                                  : '' );
    my $errstr = `mkdir -p $dir 2>&1`;
    if( $? ) {
      print STDERR "Error executing 'mkdir -p $dir' ($?):\n",
                   $errstr;
      return( undef );
    }
  
    print "Writing ${$p_typeinfo}{text} $cnt of $total ($object,dump)\n";
  
    # append the visual timestamp to the filename

    my $timestamp = ${$obj}{timestamp};
    {
      my( $second, $minute, $hour, $day, $month, $year ) =
       localtime( $timestamp );
      $filename .= sprintf( ".%4.4d-%2.2d-%2.2d_%2.2d-%2.2d-%2.2d",
                            $year + 1900, $month + 1, $day, $hour, $minute,
                            $second );
    }
  
    if( !open( OUT, ">$dir/$filename.dump" )) {
      print STDERR "Error opening ${$p_typeinfo}{text} ",
                   "'$dir/$filename.dump' for write access\n";
      return( undef );
    }
  
    setProcessing(
      output      => \*OUT,
      address     => 0,
      interrupt   => \$interrupted,
      minimize    => 0,
      number      => 0,
      process     => [ %specialProcessing ],
      'replace-*' => [ '^\(main::processDUMPdata\) ?', '', undef ],
    );
    print OUT "# NAME: $object\n",
              "# TIMESTAMP: $timestamp\n";
    dumpVar( '', $obj );
    close( OUT );
    &touchFile( $write, "$dir/$filename.dump", $timestamp );
    setProcessing(
      output      => \*STDERR,
      address     => 1,
      minimize    => 0,
      number      => 1,
      process     => undef,
      'replace-*' => [],
    );
    if( $interrupted ) {
      unlink( "$dir/$filename.dump" );
      undef $obj;
    }
  }
  return( $obj );
}

sub processProp {
  my( $type, $p_list, $base, $loc ) = @_;

  if(( ref( $base ) ne 'HASH' ) and ( ref( $base ) ne 'ARRAY' )) {
    die "Property base is not a ref to an ARRAY or HASH\n";
  }
  if( $loc =~ /(.*)\[\](.*)/ ) {
    my $front = $1;
    my $back = $2;
    my $newLoc;
    eval( '$newLoc = ${$base}'. $front );
    if( $@ ) {
      die "Error eval'ing '\$newLoc = $front':\n",
           $@;
    }

    if( ref( $newLoc ) ne 'ARRAY' ) {
      die "Property location does not point to an array: $loc\n";
    }
    foreach my $loc( @{$newLoc} ) {
      &processProp( $type, $p_list, $loc, $back );
    }
  }

  # process the prop list

  else {
    my $p_prop;
    eval( '$p_prop = ${$base}'. $loc );
    if( $@ ) {
      die "Error eval'ing '\$p_prop = $loc':\n",
           $@;
    }
    if( ref( $p_prop ) ne 'ARRAY' ) {
      die "Property location for '$type' does not point to an array: $loc\n";
    }
    for( my $p = 0; $p < @{$p_prop}; $p++ ) {
      if( !defined( ${$p_prop}[$p] =
                     &transProp2( ${$p_prop}[$p],
                                  $p_list, \%prop ))) {
        splice( @{$p_prop}, $p--, 1 );
      }
    }
  }
  return;
}

sub getData {
  my( $file, $delBad )  = @_;

  my $object;
  my $timestamp;
  if( !open( IN, $file )) {
    chomp(( $object, $timestamp ) = `/bin/head -2 $file 2>/dev/null` );
  }
  else {

    # this way is faster than 'chomp(( $object, $timestamp ) = <IN> )' because
    # only two lines are read, rather than the entire file

    chomp( $object = <IN> );
    chomp( $timestamp = <IN> );
    close( IN );
  }

  if( !( $object =~ s/^# NAME: (.*)$/$1/ )) {
    if( $delBad ) {
      print `rm -f $file`;
      return;
    }
    die "File '$file' is invalid: '^# NAME: ' is not the first line\n";
  }

  if( !( $timestamp =~ s/^# TIMESTAMP: (\d+)$/$1/ )) {
    if( $delBad ) {
      print `rm -f $file`;
      return;
    }
    die "File '$file' is invalid: '^# TIMESTAMP: ' is not the second line\n";
  }

  return( $object, $timestamp );
}

sub transProp {
  my( $prop, @p_trans ) = @_;

  my $ret = "Unknown property ($prop)";
  foreach my $p_trans( @p_trans ) {
    if( exists( ${$p_trans}{$prop} )) {
      if( !defined( ${$p_trans}{$prop} )) {
        undef $ret;
      }
      else {
        my %ret = ( actual => $prop,
                    visual => ${$p_trans}{$prop} );
        $ret = \%ret;
        $ret = ${$p_trans}{$prop};
      }
      last;
    }
  }
  return( $ret );
}

sub transProp2 {
  my( $p_prop, @p_trans ) = @_;

  if(( !exists( ${$p_prop}{prop} )) ||
     ( !exists( ${$p_prop}{value} )) || ( !exists( ${$p_prop}{valueType} ))) {
    dumpVar( 'prop', $p_prop );
    die "Property missing 'prop', 'value', and/or 'valueType'";
  }
  my $prop = ${$p_prop}{prop};
  my $p_ret;
  foreach my $p_trans( @p_trans ) {
    if( exists( ${$p_trans}{$prop} )) {
      if( !defined( ${$p_trans}{$prop} )) {
        undef $p_ret;
      }
      else {
        ${$p_ret}{${$p_trans}{$prop}}{${$p_prop}{valueType}} = ${$p_prop}{value};
      }
      last;
    }
  }
  return( $p_ret );
}

sub touchFile {
  my( $write, $file, $timestamp ) = @_;

  my( $second, $minute, $hour, $day, $month, $year ) =
   localtime( $timestamp );
  $timestamp = sprintf( "%4.4d%2.2d%2.2d%2.2d%2.2d.%2.2d",
                        $year + 1900, $month + 1, $day, $hour, $minute,
                        $second );
  if( $write ) {
    my $errstr = `/bin/touch -t $timestamp $file 2>&1`;
    if( $? ) {
      die "Error executing '/bin/touch -t $timestamp $file' ($?):\n",
          $errstr;
    }
  }
  else {
    print "/bin/touch -t $timestamp $file\n";
  }
  return;
}
