package Support::Implementation::LicenseReservesTemplate;
# 2/23/11 - Modified to deal with duplicate publishers (for VP Records)
# 4/5/11 - Added support for 'ALL' license config
# 4/13/11 - Relax date format to include 'YYYY/MM/DD' (normal is YYYY-MM-DD)
# 7/18/11 - Updated messaging if older template detected
# 7/19/11 - Updated messaging if invalid publisherID detected
# 8/5/11 - Corrected product type lookups; adjusted track lookups; added synonym for RT (ringtone)
#  Correction: Per FB14435, do _not_ allow reserves for ringtones
# 8/15/11 - Stripped whitespace from RS IDs in template
# 8/15/14 - Added Excel2007 support; skip blank lines.
# 2/24/15 - Added _normalizeDate
# 5/16/17 - Set exception if date is missing and non-penny rate license.
# 3/18/19 - Added check for invalid RS publisherID
# 3/27/19 - Make sure normalized sales date gets propagated back into row buffer
# 12/16/19 - Don't call _getDuration without trackID; strip trailing space from trackName
use strict;
use warnings;

use lib '/app/tools/common/lib';
use lib '/app/tools/rps/lib';

use IO::File;
use Data::Dumper;
use Date::Calc;

use Spreadsheet::ParseExcel;

use Common::Assert;
use Common::UTF8;
use Common::RSApp;

#use RPS::License::TrackLicense;
#use RPS::License::ReserveLiquidation;

use RPS::DB::Item::Album;
use RPS::DB::Item::Track;
use RPS::DB::Item::Master;
use RPS::DB::Item::TrackLicense;
use RPS::DB::Item::Publisher;
use RPS::DB::Item::Payor;
use RPS::DB::Item::Region;
use RPS::DB::Item::Product;
use RPS::DB::Item::ProductType;
use RPS::DB::Item::LicenseReserve;
#use RPS::DB::Item::PendingTransaction;
#use RPS::Finance::PendingTransaction;
#use RPS::DB::Item::FinanceAccount;
#use RPS::Finance::Account;

use Support::Implementation::ExcelReader;
use Support::Implementation::Excel2007Reader;
use Support::Implementation::TabDelimitedReader;

use base 'Support::Implementation::Template';

use constant kQuiet  => 0;
use constant kNormal => 1;
use constant kDebug  => 3;
my $gReportLevel = kNormal;

use constant kAlbumNotFound     => 100;
use constant kTrackNotFound     => 101;
use constant kPublisherNotFound => 102;
use constant kProductTypeAll        => 88888;
use constant kProductTypeNotFound   => 99999;

binmode STDOUT, ":utf8";

#-----------------------------------------------------------------------
# templateHeader maps column names to their default column number.
# The actual column number-header name map is stored in columnMap.
# Note(s):
# 1) Column names are case sensitive
# 2) Spaces are ignored; you must manually remove spaces from the names
#    stored in templateHeader
#-----------------------------------------------------------------------
my %gTemplateHeader = (
   "album-name"                 => 0,  # A
   "catalog-#"                  => 1,  # B
   "track-name"                 => 2,  # C
   "isrc"                       => 3,  # D
   "royaltyshare-publisher-id#" => 4,  # E
   "publisher"                  => 5,  # F
   "royaltyshare-license-id#"   => 6,  # G
   "license-config"             => 7,  # H
   "product-type"               => 8,  # I
   "region"                     => 9,  # J
   "sales-date",                => 10, # K
   "p0"                         => 11, # L
   "p1"                         => 12, # M
   "p2"                         => 13, # N
   "p3"                         => 14, # O
   "p4"                         => 15, # P
   "p5"                         => 16, # Q
   "p6"                         => 17, # R
   "p7"                         => 18, # S
#   "total"                      => 19, # T
);

#--------------------------------------------------------------
# Maps column #'s to a unique key (headername).  The reverse of
# templateHeader, but with actual column #.
#--------------------------------------------------------------
my %gColumnMap = ();

#--------------------------------------------------------
# gHeaderDisplayed is a flag that we set if we've already
# displayed the template header during an exceptions dump
#--------------------------------------------------------
my $gHeaderDisplayed;

#--------------------------------------
# count keeps track of entities created
#--------------------------------------
my %gCount = (
   license_reserve => 0,
);

#---------------------------------------------------------
## gStatRateMap contains the RSCOMMON.stat_rate information
##---------------------------------------------------------
my %gStatRateMap = ();

my $clientID;
my $execMode;

my $dbo;
my $dbh;

sub new {
   my ($class, %args) = @_;
   my $self = bless {}, $class;
   return $self->_init(%args);
}


sub _init {
   my( $self, %args ) = @_;

   report("LicenseReservesTemplate::_init -- args = ". Dumper(\%args));

   $self->SUPER::_init(%args);

   $execMode = $self->exec_mode if ( $self->exec_mode );

   return $self;
}

sub parseHeader {
   my $self = shift;
}

#--------------------------------------------------------------
# Read-in all of the template data into memory.  Once it's been
# read, try to parse it and
#--------------------------------------------------------------
sub loadMemory {
   my $self = shift;

   $clientID = $self->client_id;
   my $app = Common::RSApp->new(clientID => $clientID);

   $dbo = Common::RSApp::GetClientDB();
   $dbh = $dbo->DBH;

   my $cdbo = Common::RSApp::GetCommonDB();
   #---------------------------
   # Fill the stat_rate map
   # NB: '1' ==> US Rates
   #---------------------------
   my $sql = "SELECT * FROM stat_rate "
      . "WHERE stat_rate_type_id = 1 "
      . "ORDER BY date_effective";
   my $sth = $cdbo->DoCmd($sql);
   while( my $row = $sth->fetchrow_hashref() ) {
      my $id = $row->{stat_rate_id};
      $gStatRateMap{$id} = $row;
   }


   my $fileName = $self->name;

#   $execMode = $self->exec_mode if ( $self->exec_mode );

   if( $self->isExcel2003( $fileName ) ) {
      print("LicenseReservesTemplate::loadMemory -- loading Excel2k3 $fileName into memory\n");

      # Read in the header
      my %data;
      my $reader = Support::Implementation::ExcelReader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap
      );
      $reader->scanExcelFile;
      #_scanExcelFile( $fileName, \%data, \%gTemplateHeader, \%gColumnMap );

      # Try and parse it...
      _processData(\%data);

   } elsif( $self->isExcel2007( $fileName ) ) {
      print("LicenseTemplate::loadMemory -- loading Excel2k7 $fileName into memory\n");

      # Read in the header
      my %data;
      my $reader = Support::Implementation::Excel2007Reader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap
      );
      $reader->scanExcelFile;

      #die("Unable to import license template") if (! _validateHeader() );

      #die("loadMemory: ". Dumper(\%gColumnMap) );

      # Try and parse it...
      _processData(\%data);

   } elsif( $self->isTabDelimited( $fileName ) ) {
      report("LicenseReservesTemplate::loadMemory -- processing tab-delimited file");

      my %data;
      my $reader = Support::Implementation::TabDelimitedReader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap
      );

      $reader->scanTabbedFile;
      _processData(\%data);
   }
}

#-------------------------------------------------------------------
# _processData is where the real work is done.  It takes the generic
# information stored in the supplied array of hashes and decodes it.
# In this case, it assumes that the supplied data contains license
# data.
#-------------------------------------------------------------------
sub _processData {
   my($data) = @_;
   my $rows = $data->{rows};

   my @errorList = ();

#   my $numNewLicenses = 0;
#   my $numExceptions = 0;
#   my $numTrackExceptions = 0;
#   my $numAlbumExceptions = 0;
#   my $numLockDateExceptions = 0;
#   my $numPublisherNotFoundExceptions = 0;
#   my $numPublisherMismatchExceptions = 0;
#   my $numRateTypeExceptions = 0;
#   my $numPayorExceptions = 0;
#   my $numProductTypeExceptions = 0;
#   my $numRateBasisExceptions = 0;
#   my $numRegionExceptions = 0;
#   my $numZeroShareExceptions = 0;

   #----------------------------------
   # excount keeps track of exceptions
   #----------------------------------
   my %excount = (
      region_not_found           => 0,
      invalid_product_type       => 0,
      missing_product_type       => 0,
      invalid_licenseid          => 0,
      license_not_found          => 0,
      license_publisher_mismatch => 0,
      track_not_found            => 0,
      album_not_found            => 0,
      publisher_name_mismatch    => 0,
      product_not_found          => 0,
      no_track_duration          => 0,
   );

   #-----------------------------
   # Loop over each row (license)
   #-----------------------------

   my $rowCount=0;
   foreach my $row (@$rows) {

      ++$rowCount;

      #-------------------------------------------
      # Get all of the template variables.
      # 9/22/09: Modified to 1.0RC1 specification.
      #-------------------------------------------
      my $rowid               = $row->{rowid};

      my $albumName           = $row->{'album-name'};                  # A
      my $catalogNumber       = $row->{'catalog-#'};                   # B
      my $rsAlbumID           = $row->{'royaltyshare-album-id#'};      # C
      my $trackName           = $row->{'track-name'};                  # D
      my $isrc                = $row->{'isrc'};                        # E
      my $rsTrackID           = $row->{'royaltyshare-track-id#'};      # F

      my $rsPublisherID       = $row->{'royaltyshare-publisher-id#'};  # G
      my $publisherName       = $row->{'publisher'};                   # H
      my $rsLicenseID         = $row->{'royaltyshare-license-id#'};    # I

      #my $licenseConfig       = $row->{'license-config'};
      my $licenseConfig       = $row->{'license-product-type'};        # J

      #my $productType         = $row->{'product-type'};
      my $productType         = $row->{'reserve-configuration'};       # K

      my $region              = $row->{'region'};                      # L
      my $dateReservesTaken   = $row->{'sales-date'};                  # M
      my $p0                  = $row->{'p0'};                          # N
      my $p1                  = $row->{'p1'};                          # O
      my $p2                  = $row->{'p2'};                          # P
      my $p3                  = $row->{'p3'};                          # Q
      my $p4                  = $row->{'p4'};                          # R
      my $p5                  = $row->{'p5'};                          # S
      my $p6                  = $row->{'p6'};                          # T
      my $p7                  = $row->{'p7'};                          # U

      report("#### row($rowid) ".Dumper(\%$row));

      $rsTrackID     =~ s/\s*$//g if ( $rsTrackID );
      $rsAlbumID     =~ s/\s*$//g if ( $rsAlbumID );
      $rsPublisherID =~ s/\s*$//g if ( $rsPublisherID );
      $rsLicenseID   =~ s/\s*$//g if ( $rsLicenseID );
      $trackName     =~ s/\s*$//g if ( $trackName   );

      #-----------------------------------------------
      # errorCode will hold one or more error messages
      #-----------------------------------------------
      my $errorCode;


      my $publisherID;
      my $regionID;

      my $productTypeID;
      my $licenseConfigID; # can be a product_type, or ALL or ALL-P

      my $licenseID;
      my $licenseRateType;
      my $pennyRate;
      my $licenseRatePercentage;
      my $licenseShare;
      my $licenseRateBasis;
      my $licenseLockDate;
      my $trackID;
      my $albumID;
      my $productID;

      # Skip line if we don't have album/track/publisher
      # XXX
      next if( !$albumName && !$trackName && !$dateReservesTaken );


      #-----------------------------------
      # Check for missing template columns
      #-----------------------------------
      #die("ERROR: column \"sales-date\" not found or contains no date!\n") if ( !$dateReservesTaken );
#      if( !$dateReservesTaken )
#      {
#         _appendString( $errorCode, "Missing sales date");
#         ++$excount{missing_sales_date};
#      }

      die("ERROR: column \"p0\" not found! (should have p0 through p7)\n") if ( ! exists $row->{'p0'} );

      die("ERROR: column \"product-type\" is obsolete; see 1.0RC1 specification\n"
         . "Common errors include using 'license-config' instead of 'license-product-type' (col H), "
         . "and using 'product-type' instead of 'reserve-configuration' (col I).\n") if ( exists $row->{'product-type'} );

      #----------------------------------------------------
      # NOTE: if the licenseID is defined, should we still
      # check things like region, license config, publisher
      # and track?
      #----------------------------------------------------

      #-----------------------------------------
      # Make sure we have something to liquidate
      #-----------------------------------------
      if ( (!$p0 || '' eq $p0) &&
           (!$p1 || '' eq $p1) &&
           (!$p2 || '' eq $p2) &&
           (!$p3 || '' eq $p3) &&
           (!$p4 || '' eq $p4) &&
           (!$p5 || '' eq $p5) &&
           (!$p6 || '' eq $p6) &&
           (!$p7 || '' eq $p7) ) {
         _appendString( $errorCode, "Nothing to liquidate");
         ++$excount{nothing_to_liquidate};
      }

      #-------------------------------------------
      # Make sure that the units are whole numbers
      #-------------------------------------------
      if ( ($p0 && $p0 =~ /\./ ) ||
           ($p1 && $p1 =~ /\./ ) ||
           ($p2 && $p2 =~ /\./ ) ||
           ($p3 && $p3 =~ /\./ ) ||
           ($p4 && $p4 =~ /\./ ) ||
           ($p5 && $p5 =~ /\./ ) ||
           ($p6 && $p6 =~ /\./ ) ||
           ($p7 && $p7 =~ /\./ ) ) {
         _appendString( $errorCode, "Fractional units not allowed");
         ++$excount{fractional_units_not_allowed};
      }

      #---------------------------------------
      # Is the rsPublisherID an actual number?
      #---------------------------------------
      if ( $rsPublisherID && $rsPublisherID !~ /^\d+$/ ) {
         undef $rsPublisherID; # make sure we don't try to to a Lookup with an invalid publisherID (it works, sort of)
         _appendString( $errorCode, "RS PublisherID must be a number");
         ++$excount{invalid_rspublisherid};
      }

      #-----------------------------
      # Is the license region valid?
      #-----------------------------
      $regionID = _findRegion( name => $region );
      if ( !$regionID ) {
         _appendString( $errorCode, "Region not found");
         ++$excount{region_not_found};
      }

      #-----------------------------------------------------------------------
      # Is the product type valid?.  Note: the product type must be a tangible
      # product (e.g., not ALL or ALL-P).
      #-----------------------------------------------------------------------
      if ( !$productType ) {
         #_appendString( $errorCode, "Missing product type");
         #++$excount{missing_product_type};
         _appendString( $errorCode, "Missing reserve configuration");
         ++$excount{missing_reserve_configuration};
      } else {

         $productTypeID = _getProductTypeID( product_type => lc $productType );
         if ( !$productTypeID ||
              kProductTypeNotFound == $productTypeID ||
              RPS::DB::Item::ProductType::kAllPhysicalProducts == $productTypeID )
         {
            _appendString( $errorCode, "Invalid product type");
            ++$excount{invalid_product_type};
         }
      }

      #-----------------------------
      # Is the license config valid?
      # Note: the license config can be ALL or ALL-P
      #-----------------------------
      if ( !$licenseConfig ) { # license config _not_ in the template
         #----------------------------------------------------------------
         # If the license config wasn't specified, but there was a valid
         # product type then use the product config as the license config.
         #----------------------------------------------------------------
         if ( !$productType ) {
            _appendString( $errorCode, "Missing license config");
            ++$excount{missing_license_config};
         } else {
            if ( $productTypeID && kProductTypeNotFound != $productTypeID ) {
               $licenseConfig = $productType;
            }
         }
      }
      
      if ( $licenseConfig ) {

         $licenseConfigID = _getProductTypeID( product_type => lc $licenseConfig );

         if ( $licenseConfigID && $productTypeID && kProductTypeNotFound == $productTypeID ) {
            _appendString( $errorCode, "Invalid license config");
            ++$excount{invalid_license_config};
         }
      }

      #------------------------------------------------
      # Validate the publisher information if specified
      #------------------------------------------------
      my @publisherList; # used for dealing with duplicate publishers (TEST - VP-only FB13350)
      my $actualName;

      if ( $publisherName ) {
         if ( $rsPublisherID ) {

            my $pObj = RPS::DB::Item::Publisher->Lookup( publisher_id => $rsPublisherID );

				if( !$pObj )
				{
               report("EXCEPTION: specified RS publisherID isn't valid");
               _appendString( $errorCode, "PublisherID invalid");
               ++$excount{invalid_publisherid};
				}
				else
				{
               $actualName = $pObj->publisher_name;
               if ( $publisherName && '' ne $publisherName && $actualName ne $publisherName ) {
                  report("EXCEPTION: name on specified RS publisherID doesn't match template name!");
                  _appendString( $errorCode, "PublisherID name mismatch");
                  ++$excount{publisher_name_mismatch};
               }
               $publisherID = $rsPublisherID;
				}
         } else {
            my $sql = "SELECT publisher_id FROM publisher "
               . "WHERE publisher_name = ?"
               ;
            my $sth = $dbh->prepare($sql);
            $sth->execute($publisherName);
            if ( $sth->rows > 1 and !$rsLicenseID ) {
   
               if ( $clientID != 306 ) { # VP Records
                  _appendString( $errorCode, "Duplicate publisher name");
                  ++$excount{duplicate_publisher_name};
               } else {
                  while( my($id) = $sth->fetchrow_array() ) {
                     push @publisherList, $id;
                  }
               }
            } else {
   
               ($publisherID) = $sth->fetchrow_array();
               #$publisherID = _findPublisher(
               #   publisher_name => $publisherName
               #);
            }
         }
      }
      else
      {

         if ( $rsPublisherID ) {
            # publisher name not specified, but RS ID specified
            my $pObj = RPS::DB::Item::Publisher->Lookup( publisher_id => $rsPublisherID );

            if( !$pObj )
            {
               report("EXCEPTION: specified RS publisherID isn't valid");
               _appendString( $errorCode, "PublisherID invalid");
               ++$excount{invalid_publisherid};
            }
            else
            {
               $publisherName = $pObj->publisher_name;
               $publisherID   = $rsPublisherID;
            }
         }
         elsif ( !$rsLicenseID )
         {
            _appendString( $errorCode, "Publisher name missing");
            ++$excount{publisher_name_missing};
         }

      }

if ( $clientID != 306 ) {
      if ( ! $publisherID ) {
         report("_EXCEPTION: publisher '$publisherName' not found");
         _appendString( $errorCode, "Publisher not found");
         ++$excount{publisher_not_found};
      }
}

      my $tlObj; # this (along with $licenseID) will be set once we've found the license

      if ( $rsLicenseID ) {
         #----------------------------------------------------------------
         # If license information (aside from the licenseID) was specified,
         # make sure it's consistent with what's on the license.
         #----------------------------------------------------------------
         $tlObj = RPS::DB::Item::TrackLicense->Lookup(
            track_license_id => $rsLicenseID
         );
         if ( !$tlObj ) {
            _appendString( $errorCode, "Invalid licenseID");
            ++$excount{invalid_licenseid};
         } else {
            my $err;

            my $tlPublisherID = $tlObj->publisher_id;
            if ( $publisherID && $tlPublisherID != $publisherID ) {
               _appendString( $errorCode, "License publisher mismatch");
               ++$excount{license_publisher_mismatch};
               $err=1;
print "[$rowid] ERROR: license publisher mismatch: rsLicenseID $rsLicenseID, found pubID $tlPublisherID, expected $publisherID\n"; # XXX
            }

            $trackID = $tlObj->track_id;

            if ( !$err ) {
print "[$rowid] Found rsLicenseID $rsLicenseID, setting \$licenseID\n";
               $licenseID             = $rsLicenseID;
               $licenseRateType       = $tlObj->rate_type;
               $pennyRate             = $tlObj->penny_rate;
               $licenseRatePercentage = $tlObj->rate_percentage;
               $licenseShare          = $tlObj->share;
               $licenseRateBasis      = $tlObj->rate_basis;
               $licenseLockDate       = $tlObj->lock_date;
            }
         }

      } else {
         #---------------
         # Find the track
         #---------------

         # TODO: Need to add logic to use rsAlbumID and/or rsTrackID. 9/22

         my $errFlag;
         ($trackID,$errFlag) = _findTrack(
            catalog_number => $catalogNumber,
            album_name => $albumName,
            track_name => $trackName,
            isrc => $isrc,
         );
         if ( $trackID ) {
            report("   Found track $trackID");
         } else {
            # TODO - need to store error information
            report("_EXCEPTION: Track not found, errFlag($errFlag)");
            $row->{'error_track_name'} = "Track '$trackName' not found";
            push @errorList, $row;
            if ( $errFlag == kTrackNotFound ) {
               _appendString( $errorCode, "Track not found");
               ++$excount{track_not_found};
            }

            if ( $errFlag == kAlbumNotFound ) {
               _appendString( $errorCode, "Album not found");
               ++$excount{album_not_found};
            }
         }

         #-------------------
         # find the publisher
         # 9/23 - moved up so we check publisher no matter what
         #-------------------
         #my $actualName;
         #if ( $rsPublisherID ) {
         #   my $pObj = RPS::DB::Item::Publisher->Lookup( publisher_id => $rsPublisherID );
         #   $actualName = $pObj->publisher_name;
         #   if ( $actualName ne $publisherName ) {
         #      report("EXCEPTION: name on specified RS publisherID doesn't match template name!");
         #      _appendString( $errorCode, "PublisherID name mismatch");
         #      ++$excount{publisher_name_mismatch};
         #   }
         #   $publisherID = $rsPublisherID;
         #} else {
         #   $publisherID = _findPublisher(
         #      publisher_name => $publisherName
         #   );
         #}
         #if ( ! $publisherID ) {
         #   report("_EXCEPTION: publisher '$publisherName' not found");
         #   _appendString( $errorCode, "Publisher not found");
         #   ++$excount{publisher_not_found};
         #}

      }

      #-------------------------------------------------------
      # If we have a trackID and a valid productTypeID, try to
      # find the product.
      #-------------------------------------------------------
      if ( $trackID && $productTypeID && (kProductTypeNotFound != $productTypeID) ) {
         #----------------
         # Get the albumID
         #-----------------
         $albumID = _getTrackAlbumID($trackID) if ( $trackID );

         my $assetID;

         #-----------------
         # Find the product
         #-----------------
         if( $productTypeID == RPS::DB::Item::Product::kProductTypeDigitalTrack )
         {
             $assetID = $trackID;
         }
         else
         {
             $assetID = $albumID;
         }

         my $pObj = RPS::DB::Item::Product->Lookup(
            asset_id => $albumID,
            product_type_id => $productTypeID,
         );
         if ( !$pObj ) {
            report("DEBUG: no product found for albumID($albumID), productType($productTypeID)");
            _appendString( $errorCode, "No product found");
            ++$excount{product_not_found};
         } else {
            $productID = $pObj->product_id;
            report("DEBUG: found product($productID) for albumID($albumID), productType($productTypeID)");
         }


         if ( $clientID == 306 )
         { # VP Records

            report("INFO: VP-specific code");
            if ( !$publisherID && (scalar @publisherList > 0 ) ) {

               my $_publisherID;
               my $_licenseID;

               my $numFound=0;

               # Multiple publishers were found, so let's try to find one license
               foreach my $publisherID (@publisherList) {
                     
                  my %tlArgs = (
                     track_id => $trackID,
                     publisher_id => $publisherID,
                     region_id => $regionID,
                     product_type_id => $licenseConfigID,
                  );

                  my $tlObj = _findTrackLicense( %tlArgs );

                  if ( $tlObj ) {
                     $numFound++;

                     $_licenseID   = $tlObj->track_license_id;
                     $_publisherID = $tlObj->publisher_id;
                  }
               }#publisher loop

               if ( $numFound == 1 )
               {
                  $publisherID = $_publisherID;
                  report("   VP_DEBUG: Found unique license $_licenseID for publisher $publisherID");
               }
               elsif( $numFound == 0 )
               {
                  report("   VP_DEBUG: Found no licenses for publisher(s): ". join(", ", @publisherList) );
                  _appendString( $errorCode, "Duplicate publisher name");
                  ++$excount{duplicate_publisher_name};
               }
               else
               {
                  # This should _not_ happen. Ever.
                  die("Found $numFound licenses when one or zero was expected..");
               }
            }
            else
            {
               my $_nPub = (scalar @publisherList);

               if ( !$publisherID && $_nPub == 0 )
               {
                  #report("ERROR: VP code: publisherID($publisherID) #publisherList($_nPub)");

                  report("_EXCEPTION: publisher '$publisherName' not found");
                  _appendString( $errorCode, "Publisher not found");
                  ++$excount{publisher_not_found};
               }
            }
         }# VP Records




print "[$rowid] DEBUG: licenseID = $licenseID\n" if ( $licenseID );


         #----------------------------------------------------------------------
         # Find the track license using the track, publisher, region and product
         # type (license config).  Note: if we don't have a productID, then
         # skip looking for the license because we're won't be able to import
         # the reserve.
         #
         ## 7/11/11 - Skip license lookup if we already have it (via rsLicenseID)
         ## 8/16/11 - If for some reason rsLicenseID was set but licenseID wasn't
         # (this can happen if the publisherID information in template was
         # incorrect), then skip the license lookup.
         #----------------------------------------------------------------------
         #if ( $publisherID && $regionID && $licenseConfigID && $productID )
         if ( !$licenseID && !$rsLicenseID && $publisherID && $regionID && $licenseConfigID && $productID )
         {

            my %tlArgs = (
               track_id => $trackID,
               publisher_id => $publisherID,
               region_id => $regionID,
               product_type_id => $licenseConfigID,
            );
print "D: Calling _findTrackLicense - tlArgs = ". Dumper(\%tlArgs) . "\n"; # XXX

            my $tlObj = _findTrackLicense( %tlArgs );

            if ( $tlObj ) {
               $licenseID             = $tlObj->track_license_id;
               $licenseRateType       = $tlObj->rate_type;
               $pennyRate             = $tlObj->penny_rate;
               $licenseRatePercentage = $tlObj->rate_percentage;
               $licenseShare          = $tlObj->share;
               $licenseRateBasis      = $tlObj->rate_basis;
               $licenseLockDate       = $tlObj->lock_date;
               report("   Found license $licenseID");

               # Store the information in the data row.
               $row->{'royaltyshare-license-id#'} = $tlObj->track_license_id;
               $row->{'license-config'} = _printProductType($tlObj->product_type_id);

            } else {
               report("   WARNING: License not found : ". Dumper(\%tlArgs));
               _appendString( $errorCode, "License not found");
               ++$excount{license_not_found};

               if ( $clientID == 306 ) { # VP Records
                  # Sanity check - are there _any_ licenses on the track?
                  my $sql = "SELECT track_license_id FROM track_license WHERE track_id=$trackID";
                  my $sth = $dbo->DoCmd($sql);
                  if ( $sth->rows == 0 ) {
                     report("   VP_WARNING: No licenses setup on trackID $trackID");
                  } else {
                     report("   VP_WARNING: License not found, but trackID $trackID has ". $sth->rows . " other license(s)");
                  }
               }



            }

         }
         else
         {
            if( !$licenseID && !$rsLicenseID )
            {
               # License not found
               my $_pubID  = ($publisherID) ? $publisherID : "---";
               my $_prodID = ($productID) ? $productID : "";
               my $_regID  = ($regionID) ? $regionID : "";
               my $_cfgID  = ($licenseConfigID) ? $licenseConfigID : "";
               print "WARNING[$rowid]: No license found: pubID($_pubID) rg($_regID) licConfig($_cfgID) prod($_prodID)\n";
            }
         }

      }
      else
      {
         # No matching product found

         report("DEBUG:WARNING: Skipping product lookup, trackID(". ($trackID || "") .") productTypeID(". ($productTypeID || "") . ")");
      }

      # We need the date for sale rate basis with non-penny rate licenses
      if( !$dateReservesTaken &&
          $licenseRateType && RPS::DB::Item::TrackLicense::kRateTypePenny != $licenseRateType &&
          $licenseRateBasis && RPS::DB::Item::TrackLicense::kRateBasisSale != $licenseRateBasis )
      {
          _appendString( $errorCode, "Missing sales date");
          ++$excount{missing_sales_date};
      }
     
#      #----------------------------------------------
#      # At this point, the row is either good or bad.
#      #----------------------------------------------
#      if ( $errorCode ) {
#         $row->{'error-code'} = $errorCode;
#         next;
#      }

      # Normalize date if present
      #
      if ( $dateReservesTaken ) {
          my $_date = _normalizeDate( $dateReservesTaken );
          $row->{'sales-date'} = $_date if ( $_date ne $dateReservesTaken );  # store normalized date in case of exception
      }

      #----------------------------------------------
      # Create the reserve(s) if there were no errors
      #----------------------------------------------
      my $statRate;
      my $effRate;
      my $year;

      my $duration = _getDuration( $trackID ) if ( $trackID );

      my $lookupDate;
      if ( RPS::DB::Item::TrackLicense::kRateBasisSale == $licenseRateBasis && $dateReservesTaken )
      {
         $lookupDate = _normalizeDate( $dateReservesTaken );
      }
      elsif ( RPS::DB::Item::TrackLicense::kRateBasisLock == $licenseRateBasis  )
      {
         assert($licenseLockDate);
         $lookupDate = _normalizeDate( $licenseLockDate );
      }
      else
      { 
         assert("unknown rate basis");
      }

      #----------------------------------------------
      # At this point, the row is either good or bad.
      #----------------------------------------------
      if ( $errorCode ) {
         $row->{'error-code'} = $errorCode;
         next;
      }


      my ( $statRateID, $rate, $minRate );
      if ( RPS::DB::Item::TrackLicense::kRateTypePenny != $licenseRateType )
      {
          ( $statRateID, $rate, $minRate ) = _getStatRateID( $lookupDate );
      }

      # XXX Do we need a statRateID w/ penny licenses?  E.g., will the reserve be processed
      # properly without a stat_rate_id being present?

      if ( RPS::DB::Item::TrackLicense::kRateTypeFull == $licenseRateType )  {

         #------------------------------------------------------------
         # Sanity check -- if we're processing a full-stat license and
         # no duration is available, exception out because we won't be
         # able to calculate the effective rate for the reserve.
         #------------------------------------------------------------
         if ( !$duration || $duration == 0 ) {
            report("WARNING: license $licenseID is a full-stat license, "
               ."however trackID $trackID has no duration.");
            _appendString( $errorCode, "No track duration");
            ++$excount{no_track_duration};
            $row->{'error-code'} = $errorCode;
            next;
         }

         if ( $duration > 300 ) { # five minutes
            $statRate = $duration * $minRate;
         } else {
            $statRate = $rate;
         }
      } elsif ( RPS::DB::Item::TrackLicense::kRateTypeMinimum == $licenseRateType )  {
         $statRate = $rate;
      } else {
         $statRate = $pennyRate;
      }

#      #--------------------------------
#      # Determine the stat rate indexes
#      #--------------------------------
#      if ( RPS::DB::Item::TrackLicense::kRateBasisSale == $licenseRateBasis ) {
#         $year = _getYear($dateReservesTaken);
#      } elsif ( RPS::DB::Item::TrackLicense::kRateBasisLock == $licenseRateBasis  ) {
#         assert($licenseLockDate);
#         $year = _getYear($licenseLockDate);
#      } else { 
#         assert("unknown rate basis");
#      }
#      my $statRateID = _getStatRateID($year);

      #-------------------------------------------------------------------
      # Calculate the effective rate that the reserves will be released at
      #-------------------------------------------------------------------
      $effRate = $statRate * ($licenseRatePercentage/100) * ($licenseShare/100);
      if ( RPS::DB::Item::TrackLicense::kRateTypePenny != $licenseRateType )  {
          $effRate = $statRate * ($licenseRatePercentage/100) * ($licenseShare/100);
      } else {
          $effRate = $statRate;
      }

      assert( $effRate > 0 );

      my @buckets;
      push @buckets, $p0;
      push @buckets, $p1;
      push @buckets, $p2;
      push @buckets, $p3;
      push @buckets, $p4;
      push @buckets, $p5;
      push @buckets, $p6;
      push @buckets, $p7;

      #------------------------------------------------------
      # reserveList will contain a list of reserveIDs created
      #------------------------------------------------------
      my @reserveList;
      my $per=1;
      foreach my $units (@buckets) {
         if ( $units ) {
            my $reserveID = _createReserve(
               license_id     => $licenseID,
               product_id     => $productID,
               effective_rate => $effRate,
               units          => $buckets[$per - 1],
               period         => $per,
               stat_rate_id   => $statRateID,
               rate_basis     => $licenseRateBasis,
            );
            push @reserveList, $reserveID if ( $reserveID );

         }
         ++$per;
      }
      my $zzReserveList = join(",", @reserveList);
      $row->{'rs-reserve-id'} = $zzReserveList if ( $zzReserveList );


   }# row loop

   report("_processData: ". @errorList . " row(s) had errors");

   #------------------------------------------------------------
   # Validation: Do we have at least one of the periods defined?
   #------------------------------------------------------------

   _showExceptions( $rows );  # XXX XXX XXX

   #-----------------------
   # Show import statistics
   #-----------------------
   report("##### S U M M A R Y #####");
   my $totalExceptions = 0;
   my $totalRows = (scalar @$rows);

   foreach my $c (keys %excount) {
      my $v = $excount{$c};
      printf("%30s %6d\n", $c, $v);
      $totalExceptions += $v;
   }
   printf("%30s %s\n", " ", "-------" );
   printf("%30s %6d\n", "Total Exceptions", $totalExceptions );
   printf("%30s %6d\n", "Total Rows", $totalRows );
   report(" ");
   
   foreach my $c (keys %gCount) {
      my $v = $gCount{$c};
      printf("%30s %d\n", $c, $v);
   }
   report("total rows = $rowCount");
}#_processData

#--------------------------------------------------------------------------
# _showExceptions was originally intended to _just_ show the template lines
# that exceptioned out.  It's been modified to output both exception and
# non-exception lines.
#--------------------------------------------------------------------------
sub _showExceptions {
   my ( $rows ) = @_;

   #---------------------------
   # Build the exception header
   #---------------------------
   my @header;
   for( my $i = 0; $i < (keys %gColumnMap); $i++ ) {
      # Get the key at the specified column
      my $v = $gColumnMap{$i};

      #----------------------------------------------------------
      # Do _not_ push the "Error Code" or "import-status" columns
      # if they were in the original template.  We'll re-create
      # those columns below.
      #----------------------------------------------------------
      next if ( ("Error Code" eq $v) || ("import-status" eq $v) );

      push @header, $v;
   }
   report("STATUS:\t".join("\t", @header, "Error Code", "import-status"));

   #--------------------------------------
   # Now dump out the rows that had errors
   #--------------------------------------
   foreach my $row (@$rows) {
      my $rowid = $row->{rowid};
      my $errorCode = $row->{'error-code'};
      my $reserveID = $row->{'rs-reserve-id'} || '---';

      #----------------------------------------
      # Output the row data in the proper order
      #----------------------------------------
      my @obuf;
      for( my $i = 0; $i < (keys %gColumnMap); $i++ ) {
         # Get the key at the specified column
         my $v = $gColumnMap{$i};
         my $val = ($row->{$v}) ? $row->{$v} : '';

         #----------------------------------------------------------
         # Do _not_ push the "Error Code" or "import-status" columns
         # if they were in the original template.  We'll re-create
         # those columns below.
         #----------------------------------------------------------
         next if ( ("Error Code" eq $v) || ("import-status") eq $v );

         push @obuf, $val;
      }

      my $importStatus = ($errorCode) ? "__FAIL__" : "reserve($reserveID)";

      my $ecString = ($errorCode) ? $errorCode : '';

      my $zzbuf = join("\t", "STATUS:", @obuf, $ecString, $importStatus);

      report($zzbuf);
   }
}

#----------------------------------------------------------------------------
# Look for a track license with the specified configuration.  If a specific
# configuration can't be matched, look for an 'all' or 'all-physical' license.
#----------------------------------------------------------------------------
sub _findTrackLicense {
   my(%args) = @_;
   my $trackID       = $args{track_id};
   my $publisherID   = $args{publisher_id};
   my $regionID      = $args{region_id};
   my $productTypeID = $args{product_type_id};
   assert($trackID);
   assert($publisherID);
   assert($regionID);
   assert($productTypeID);

   # Find the license
   #
   my $sql = "SELECT track_license_id FROM track_license "
      . "WHERE track_id=$trackID "
      . "AND publisher_id=$publisherID "
      . "AND region_id=$regionID ";

   if ( $productTypeID == kProductTypeAll ) {
      $sql .= "AND product_type_id IS NULL ";
   } else {
      $sql .= "AND product_type_id = $productTypeID ";
   }

   my $sth = $dbo->DoCmd($sql);
   die("Too many licenses found: sql =\n$sql") if ( $sth->rows > 1 );

   my $tlObj;
   if ( $sth->rows == 1 ) {
      my($id) = $sth->fetchrow_array();
      $tlObj = RPS::DB::Item::TrackLicense->Lookup( track_license_id => $id );
   }

#   my %tlArgs = (
#      track_id        => $trackID,
#      publisher_id    => $publisherID,
#      region_id       => $regionID,
#      product_type_id => $productTypeID,
#   );
#   my $tlObj = RPS::DB::Item::TrackLicense->Lookup( %tlArgs );
#
#   if ( !$tlObj ) {
#      #-------------------------------------------------------------------
#      # Didn't find product-specific license; is there an 'all' or 'all-p'
#      # license?
#      #-------------------------------------------------------------------
#      my $sql = "SELECT track_license_id FROM track_license "
#         . "WHERE track_id=$trackID "
#         . "AND publisher_id=$publisherID "
#         . "AND region_id=$regionID "
#         . "AND (product_type_id IS NULL OR product_type_id = 255)";
#      my $sth = $dbo->DoCmd($sql);
#      die("Too many licenses found: sql =\n$sql") if ( $sth->rows > 1 );
#
#      if ( $sth->rows == 1 ) {
#         my($id) = $sth->fetchrow_array();
#         $tlObj = RPS::DB::Item::TrackLicense->Lookup( track_license_id => $id );
#      }
#   }

   return $tlObj;
}

#--------------------------------
# Create a license_reserve entry.
#--------------------------------
sub _createReserve {
   my (%args) = @_;
   my $reserveID;

   my $units      = $args{units};
   return if ( !$units  || $units == 0);

   my $licenseID  = $args{license_id};
   my $productID  = $args{product_id};
   my $effRate    = $args{effective_rate};
   my $period     = $args{period};
   my $statRateID = $args{stat_rate_id};
   my $rateBasis  = $args{rate_basis};
   assert($licenseID);
   assert($productID);
   assert($effRate);
   assert(defined $period);
   #assert($statRateID);
   assert($rateBasis);

   my %rargs = (
      track_license_id           => $licenseID,
      product_id                 => $productID,
      original_statement_item_id => 0,
      units                      => $units,
      effective_rate             => $effRate,
      periods_remaining          => $period,
      #sale_stat_rate_id          => $statRateID,
   );

   $rargs{sale_stat_rate_id} = $statRateID if( $statRateID );

   $rargs{issue_stat_rate_id} = $statRateID
      if ( RPS::DB::Item::TrackLicense::kRateBasisLock == $rateBasis );

   if ( $execMode ) {
      my $rObj = RPS::DB::Item::LicenseReserve->Create( %rargs );
      $rObj->save();
      $reserveID = $rObj->license_reserve_id;
      report("   Created license_reserve $reserveID : ".Dumper(\%rargs));
      ++$gCount{license_reserve};
   } else {
      report("   Non-exec mode, skipping reserve : ".Dumper(\%rargs));
   }

   report("   _createReserves: effRate($effRate) licenseID($licenseID) "
      ."productID($productID) units($units) period($period) "
      ."statRateID(" . ($statRateID || 'UNDEF') .")"
   );
   return $reserveID;

}# _createReserve

# Note: this really needs to be a common method shared by all importers
sub _normalizeDate {
   my( $dt ) = @_;
   print "D: _normalizeDate: dt($dt)\n"; # XXX
   my $date;
   if ( $dt =~ m/^(\d+)$/ ) {
      my $_dt = Spreadsheet::ParseExcel::Utility::ExcelFmt( "yyyy-mm-dd", $dt );
      report("_normalizeDate:  Converted msft date $dt to $_dt");
      $date = $_dt;
   }
   elsif( $dt =~ /^(\d{1,2})-(\d{1,2})-(\d{2})$/ ) # MM-DD-YY
   {
   print "D: _normalizeDate: form 1\n"; # XXX
      $date = sprintf("%04d-%02d-%02d", ($3 + 2000), $1, $2);
   }
   elsif( $dt =~ /^(\d{4})[-\/](\d{2})[-\/](\d{2})$/ )
   {
      $date = $dt;
      print "D: _normalizeDate: form 4\n"; # XXX
   }
   elsif( $dt =~ /(-|\/)/ )  # date has hyphens or slash ...
   {
      $dt =~ s/-//g;
      $dt =~ s/\///g;

      if( $dt =~ /^(\d{4})(\d{2})$/ ) # YYYYMM
      {
         $date = sprintf("%04d-%02d-%02d", $1, $2, 1 );
   print "D: _normalizeDate: form 2\n"; # XXX
      }
      elsif( $dt =~ /^(1|2)(\d{3})$/ ) # 1YYY or 2YYY
      {
         $date = sprintf("%04d-%02d-%02d", $dt , 1, 1 );
   print "D: _normalizeDate: form 3\n"; # XXX
      }
      else
      {
   print "D: _normalizeDate: unknown form '$dt'\n"; # XXX
      }
   }
   else
   {
   print "D: _normalizeDate: form 5\n"; # XXX
      $date = $dt;
   }
   return $date;
}# _normalizeDate

sub _getStatRateID_OBE {
   my ( $t_date ) = @_;
   assert($t_date);

   my $t_stat_rate_id;

   my $t_year = $t_date;
   if ( $t_year eq '1996' or $t_year eq '1997' ) {
      $t_stat_rate_id = 11;  # see RSCOMMON.stat_rate for more info
   } elsif ( $t_year eq '1998' or $t_year eq '1999' ) {
      $t_stat_rate_id = 12;
   } elsif ( $t_year eq '2000' or $t_year eq '2001' ) {
      $t_stat_rate_id = 13;
   } elsif ( $t_year eq '2002' or $t_year eq '2003' ) {
      $t_stat_rate_id = 14;
   } elsif ( $t_year eq '2004' or $t_year eq '2005' ) {
      $t_stat_rate_id = 15;
   } elsif ( $t_year eq '2006' or $t_year eq '2007' or $t_year eq '2008' ) {
      $t_stat_rate_id = 16;
   } else {
      die("   _getStatRateID: Encountered unknown year '$t_year', ".
             "defaulting to max rate");
      $t_stat_rate_id = 16;
   }
   return $t_stat_rate_id;
}

sub _getStatRateID {
   my ( $t_date ) = @_;
   assert($t_date);

   my %rateTypeMap = (
      1 => 'US',
      2 => 'CA',
      3 => 'RINGTONE',
   );

   my($actualStatRateID, $actualRate, $actualMinuteRate) = ("", "", "");

   $t_date =~ s/-//g if ( $t_date =~ m/-/ );
   $t_date =~ s/\///g if ( $t_date =~ m/\// );

report("DEBUG: _getStatRateID: t_date($t_date)");

   foreach my $statRateID (sort{$a <=> $b} keys %gStatRateMap) {
      my $row = $gStatRateMap{$statRateID};

      my $rateType   = $row->{stat_rate_type_id};
      my $effDate    = $row->{date_effective};
      my $rate       = $row->{rate};
      my $minRate    = $row->{minute_rate};
      my $sRateType  = $rateTypeMap{$rateType};

      my $dateInfo = "";
      $effDate =~ s/\-//g;

      if ( $t_date > $effDate ) {
         $actualStatRateID = $statRateID;
         $actualRate = $rate;
         $actualMinuteRate = $minRate;
         $dateInfo = "***"
      }

      report("  statRateID($statRateID) rate($rate) minRate($minRate) "
         . "dateEffective($effDate) type($sRateType) $dateInfo");
   }

   die("_getStatRateID: Unable to find stat_rate_id for date '$t_date'")
      if ( !$actualStatRateID );

   return ($actualStatRateID, $actualRate, $actualMinuteRate);
}

#------------------------------------------------
# _getDuration -- returns the duration in seconds
#------------------------------------------------
sub _getDuration {
   my($trackID) = @_;
   my $d;
   my $sql = qq(
      SELECT m.duration
      FROM track t JOIN master m USING(master_id)
      WHERE t.track_id=$trackID
   );
   my $sth = $dbo->DoCmd($sql);
   ($d) = $sth->fetchrow_array();
   return $d;
}

#------------------------------------------------------------
# _getRoyaltyRate -- returns the royalty rate and minute rate
#  for a given date
#------------------------------------------------------------
sub _getRoyaltyRate {
   my($dateTaken) = @_;
   my $rate;
   my $minRate;
   assert($dateTaken);

   my $year = _getYear($dateTaken);
   #if ( $dateTaken =~ m/^(\d\d\d\d)/ ) { $year = $1; }
   #if ( $dateTaken =~ m/(\d\d\d\d)$/ ) { $year = $1; }
   die("invalid date '$dateTaken'") if ( !$year );

   if ( $year >= 2006 ) {
      $rate    = 0.0910;
      $minRate = 0.0175;
   } elsif( $year >= 2004 ) {
      $rate    = 0.0850;
      $minRate = 0.0165;
   } elsif( $year >= 2002 ) {
      $rate    = 0.0800;
      $minRate = 0.0155;
   } elsif( $year >= 2000 ) {
      $rate    = 0.0755;
      $minRate = 0.0145;
   } else {
      die("_getRoyaltyRate: WTF -- reserves from $dateTaken ???");
   }

   return($rate, $minRate);
}

sub _getYear {
   my($dt) = @_;
   my $year;
   if ( $dt =~ m/^(\d\d\d\d)/ ) { $year = $1; }
   if ( $dt =~ m/(\d\d\d\d)$/ ) { $year = $1; }
   return $year;
}

#-------------------------------------------------------------
# _dumpError -- go through all of the TrackLicense properties
# and display the current value (if any), underlying datatype,
# and error information
#-------------------------------------------------------------
sub _dumpError {
   my($leader, $license) = @_;
   #report("_dumpError: received ". ref $license );

   #-------------------------------------------------------
   # The following will show _all_ properties (not just the
   # usual track_license info)
   #-------------------------------------------------------
   foreach my $k (sort { $a cmp $b } keys %$license ) {

      my $v = $license->{$k};
      my $typeOf = ref $v;

      my $hasError = '';
      if ( (ref $v) =~ /Common::FormObject::Scalar/ ||
           ref $v eq "Common::FormObject::DateTime" ) {
         #my $sv = $v->_getValue() || '';  # _getValue doesn't return 0?
         my $sv = (defined $v->_getValue()) ? $v->_getValue() : '__undef__';
         $hasError = $v->_hasError();
         my $errFlag = $hasError ? "*** ERROR ***" : '';
         printf("%s   %20s  %20s %s %s\n", $leader, $k, $sv, $typeOf, $errFlag);

      } elsif( (ref $v) =~ /RPS::Finance::PendingTransactionList/ ) {
         #printf("%s   %20s  %20s %s\n", $leader, $k, " ", $typeOf);
         my $pArray = $v->getPendingTransactionArray();
         my $pType = ref $pArray;
         $hasError = $v->_hasError();
         my $errFlag = $hasError ? "*** ERROR ***" : '>> OK <<';

         #printf("%s   %20s  %20s %s pTYPE=%s\n", $leader, $k, " ", $typeOf, $pType );

         printf("%s %s %s\n", $leader, $typeOf, $errFlag );
         foreach my $pt (@{$pArray}) {
            my $amt = $pt->Amount();
            my $typeCode = $pt->TypeCode();
            $pType = ref $pt;
            $hasError = $pt->_hasError();
            my $errFlag = $hasError ? "*** ERROR ***" : ' >> OK <<';
            #printf("%s   %20s  %20s %s %s\n", $leader, $amt, $typeCode, $pType, $errFlag );
            report("$leader    $pType: amount($amt) typeCode($typeCode) $errFlag");
         }

      } elsif( (ref $v) =~ /RPS::Finance::Account/ ) {
         $hasError = $v->_hasError();
         my $errFlag = $hasError ? "*** ERROR ***" : '>> OK <<';

         printf("%s   %20s  %20s %s %s\n", $leader, $k, " ", $typeOf, $errFlag);
         my $financeAccountID = $v->AccountID();
         #printf(" accountID(%d)\n", $financeAccountID );
         _dumpError( $leader . "      ",\%$v );

      } elsif( (ref $v) =~ /RPS::License::ReserveLiquidationList/ ) {
         #my $reserveLiquidationList = $v->{ReserveLiquidationList};
         #my $reserveArray = $reserveLiquidationList->getList();
         my $reserveArray = $v->getList();
            
         #printf("%s   %20s  %20s %s\n%20s", $leader, $k, " ", $typeOf);
         printf("%s   %20s  %s\n%20s", $leader, $k, "@", $typeOf);
         for( my $i=0; $i<8; $i++ ) {
         #   if ( $reserveArray->[$i]->Percent() ) {
               printf("%s [%d] %d,", $leader, $i, $reserveArray->[$i]->Percent() );
         #   }
         }
         print"\n";
      } else {
         #printf("DEBUG: leader($leader)\n");
         #printf("DEBUG: k($k)\n");
         #printf("DEBUG: v($v)\n");
         #printf("DEBUG: typeOf($typeOf)\n");
         my $errFlag = '';
#         if( defined $v ) {
#            $hasError = $v->_hasError();
#            $errFlag = $hasError ? "*** ERROR ***" : '>> OK <<';
#         }

         $v = "---" if ( !$v );
         printf("%s   %20s  %20s %s %s\n", $leader, $k, $v, $typeOf, $errFlag);
      }
   }
}# _dumpError

sub _dumpError2 {
   my($leader, $hash) = @_;
   #-------------------------------------------------------
   # The supplied hash is assumed to be a hash of form
   # elements.  Check each one for an error...
   #-------------------------------------------------------
   foreach my $k (sort { $a cmp $b } keys %$hash ) {
      report("_dumpError2: examining k($k)");
      my $v = $hash->{$k};
      my $typeOf = ref $v;

      my $hasError = '';

      if ( (ref $v) =~ /Common::FormObject::Scalar/ ||
           (ref $v) eq "Common::FormObject::DateTime" ) {
         _reportError($k, $v );
      }
   }

}# _dumpError2

sub _getTrackAlbumID {
   my($trackID) = @_;
   my $tObj = RPS::DB::Item::Track->Lookup( track_id => $trackID );
   return $tObj->album_id;
}

sub _findTrack {
   my(%args) = @_;
   my $trackID;
   my ($albumID,$errflag) = _findAlbum(%args);
   if ( ! $albumID ) {
      report("   _findTrack: album not found, skipping track");
      $errflag = kAlbumNotFound;
   } else {
      my $trackName = $args{track_name};

      # 8/5/11 - Leave trailing spaces. -ES
      #$trackName =~ s/\s+$//; # remove trailing spaces

      my $isrc = (defined $args{isrc}) ? $args{isrc} : "";
      report("   _findTrack: looking for track($trackName) isrc($isrc) on albumID($albumID)");

      my $tColl = RPS::DB::Item::Track->GetTracksByAlbumID($albumID);
      while( $tColl->hasNext() ) {
         my $trackObj = $tColl->next();
         if ( $isrc ) {
            my $masterID = $trackObj->master_id;
            my $mObj = RPS::DB::Item::Master->Lookup( master_id => $masterID );
            my $zzIsrc = $mObj->isrc;
            if ( $isrc && $zzIsrc && $isrc eq $zzIsrc ) {
               $trackID = $trackObj->track_id;
               last;
            }
         } else {
            if ( (lc $trackName) eq (lc $trackObj->title) ) {
               $trackID = $trackObj->track_id;
               last;
            }
         }
      }
      $errflag = kTrackNotFound if( !$trackID );
   }
   return ($trackID, $errflag);
}# _findTrack

sub _findAlbum {
   my(%args) = @_;
   my $catNo = $args{catalog_number};
   my $albumName = $args{album_name};
   my $errflag;
   my $albumID;
   if ( $albumName && $catNo ) {
      report("   _findAlbum: looking for CAT#($catNo) title($albumName)");
      my $aObj = RPS::DB::Item::Album->Lookup(
         title => $albumName,
         catalog_number => $catNo,
      );
      if ( $aObj ) {
         $albumID = $aObj->album_id;
         report("   _findAlbum: found albumID($albumID)");
      } else {
         report("   _findAlbum: album not found");
         $errflag = 100;
      }
   }
   return ($albumID, $errflag);
}# _findAlbum

sub _findPublisher {
   my(%args) = @_;
   my $publisherID;
   my $pObj = RPS::DB::Item::Publisher->Lookup(
      publisher_name => $args{publisher_name},
   );
   # TODO: Should check for duplicate publisher names..
   if ( $pObj ) {
      $publisherID = $pObj->publisher_id;
   }
   return $publisherID;
}

sub _findPayor {
   my(%args) = @_;
   my $payorID;
   my $pObj = RPS::DB::Item::Payor->Lookup(
      name => $args{payor_name},
   );
   if ( $pObj ) {
      $payorID = $pObj->payor_id;
   }
   return $payorID;
}

sub _findRegion {
   my(%args) = @_;
   my $regionID;
   my $regionName = $args{name};

   # hack -- need an external way of transforming
   # region names
   $regionName = "United States" if ( $args{name} eq 'DOMESTIC' );

   my $rObj = RPS::DB::Item::Region->Lookup(
      name => $regionName,
   );
   if ( $rObj ) {
      $regionID = $rObj->region_id;
   }
   return $regionID;
}

sub _printProductType {
   my($id) = @_;
   my %types = (
      RPS::DB::Item::ProductType::kAllDigitalProducts => 'ALL-D',
      RPS::DB::Item::ProductType::kAllPhysicalProducts => 'ALL-P',
      RPS::DB::Item::Product::kProductTypeLP => 'LP',
      RPS::DB::Item::Product::kProductTypeLP5 => 'LP5',
      RPS::DB::Item::Product::kProductTypeCD => 'CD',
      RPS::DB::Item::Product::kProductTypeDigital => 'D',
      RPS::DB::Item::Product::kProductTypeDigitalTrack => 'DT',
      RPS::DB::Item::Product::kProductTypeVHS => 'VHS',
      RPS::DB::Item::Product::kProductTypeCass => 'CASS',
      RPS::DB::Item::Product::kProductTypeEP => 'EP',
      RPS::DB::Item::Product::kProductTypeDVD => 'DVD',
      RPS::DB::Item::Product::kProductTypeCDSingle => 'CD5',
      RPS::DB::Item::Product::kProductTypeCassSingle => 'CAS5',
      RPS::DB::Item::Product::kProductTypeDVDCDSet => 'DVDCD',
      RPS::DB::Item::Product::kProductTypeDblCD => 'CD2',
      RPS::DB::Item::Product::kProductTypeRingtone => 'RING',
   );

   if (!$id or !$types{$id}) {
      return 'ALL';
   } else {
      return $types{$id};
   }
}

#sub _getProductTypeID {
#   my(%args) = @_;
#   my $cfg = $args{product_type};
#report("DEBUG: _getProductTypeID -- cfg($cfg)");
#   #return RPS::DB::Item::ProductType::kAllProducts if ( $cfg =~ /^all$/i );
#   return undef if ( $cfg =~ /^all$/i );
#   return RPS::DB::Item::ProductType::kAllPhysicalProducts if ( ($cfg =~ /^allp$/i) || ($cfg =~ /^all-p$/i) );
#   return RPS::DB::Item::Product::kProductTypeLP if ( $cfg =~ /^lp$/i );
#   return RPS::DB::Item::Product::kProductTypeCD if ( $cfg =~ /^cd$/i );
#   return RPS::DB::Item::Product::kProductTypeDigital if ( $cfg =~ /^d$/i );
#   return RPS::DB::Item::Product::kProductTypeVHS if ( $cfg =~ /^vhs$/i );
#   return RPS::DB::Item::Product::kProductTypeCass if ( $cfg =~ /^cass$/i );
#   return RPS::DB::Item::Product::kProductTypeEP if ( $cfg =~ /^ep$/i );
#   return RPS::DB::Item::Product::kProductTypeDVD if ( $cfg =~ /^dvd$/i );
#   return RPS::DB::Item::Product::kProductTypeCDSingle if ( $cfg =~ /^cd5$/i );
#   return RPS::DB::Item::Product::kProductTypeCassSingle if ( $cfg =~ /^cas5$/i );
#   return RPS::DB::Item::Product::kProductTypeDVDCDSet if ( $cfg =~ /^dvdcd$/i );
#   return RPS::DB::Item::Product::kProductTypeDblCD if ( $cfg =~ /^cd2$/i );
#   return kProductTypeNotFound;
#}

sub _getProductTypeID {
   my(%args) = @_;
   my $cfg = $args{product_type};
   #return RPS::DB::Item::ProductType::kAllProducts if ( $cfg =~ /^all$/i );
   return kProductTypeNotFound if ( !$cfg );
   #return undef if ( $cfg =~ /^all$/i );
   return kProductTypeAll if ( $cfg =~ /^all$/i );

   return RPS::DB::Item::ProductType::kAllDigitalProducts if ( ($cfg =~ /^alld$/i) ||
      ($cfg =~ /^all-d$/i) || ($cfg =~ /^all digital$/i) || ($cfg =~ /^all-digital$/i) ); # All digital

   return RPS::DB::Item::ProductType::kAllPhysicalProducts if ( ($cfg =~ /^allp$/i) ||
      ($cfg =~ /^all-p$/i) || ($cfg =~ /^all physical$/i ) );

   return RPS::DB::Item::Product::kProductTypeLP           if ( $cfg =~ /^lp$/i );
   return RPS::DB::Item::Product::kProductTypeCD           if ( $cfg =~ /^cd$/i );
   return RPS::DB::Item::Product::kProductTypeDigital      if ( $cfg =~ /^da$/i );
   return RPS::DB::Item::Product::kProductTypeDigitalTrack if ( $cfg =~ /^dt$/i );
   return RPS::DB::Item::Product::kProductTypeVHS          if ( $cfg =~ /^vhs$/i );
#   return RPS::DB::Item::Product::kProductTypeCass         if ( $cfg =~ /cass(?:ette)/i );
   return RPS::DB::Item::Product::kProductTypeCass         if ( $cfg =~ /^(cass|cassette)$/i );
   return RPS::DB::Item::Product::kProductTypeEP           if ( $cfg =~ /^ep$/i );
   return RPS::DB::Item::Product::kProductTypeDVD          if ( $cfg =~ /^dvd$/i );
   return RPS::DB::Item::Product::kProductTypeCDSingle     if ( $cfg =~ /^cd5$/i );
   return RPS::DB::Item::Product::kProductTypeCassSingle   if ( $cfg =~ /^cas5$/i );
   return RPS::DB::Item::Product::kProductTypeDVDCDSet     if ( $cfg =~ /^dvdcd$/i );
   return RPS::DB::Item::Product::kProductTypeDblCD        if ( $cfg =~ /^cd2$/i );
   #return RPS::DB::Item::Product::kProductTypeRingtone     if ( $cfg =~ /^ring$/i || $cfg =~ /^rt$/ );  # FB14435
   return RPS::DB::Item::Product::kProductTypeLP5          if ( $cfg =~ /^lp5$/i );
print "D: Unable to determine product type for '$cfg'\n"; # XXX XXX
   return kProductTypeNotFound;
}

sub _appendString {
   my($str,$v) = @_;

   if ( $str ) {
      my $cur = $str;
      my $newstring = "$cur; $v";
      $_[0] = $newstring;
   } else {
      $_[0] = $v;
   }
}# _appendString

sub _reportError {
   my ($name, $obj) = @_;

   report("   _reportError: checking '$name' for errors");
   if ($obj && $obj->_hasError()) {
      #my ($e, $msg) = $obj->getError();
      #print "$name has an error: $e : $msg\n";

      my %xmlParams = $obj->getXMLParams();
      my $msg = defined $xmlParams{emsg} ? $xmlParams{emsg} : "MSG_NOT_AVAILABLE";
      my $e = $xmlParams{e};
      print "$name has an error:: $e :: $msg\n";
      return 1;
   }
   return undef;
}

sub report {
   my($text, $level) = @_;
   $level = kNormal unless $level;
   if ( $level <= $gReportLevel ) {
      print $text . "\n";
   }
}

1;
