package Support::Implementation::LicenseCommon;
#
# 11/13/09 -- Added support for All Digital (ALL-D) and DT product types.
# 3/12/10 -- Changed 'D' to 'DA' in _getProductTypeID()
# 4/8/10 -- Added 'royaltyshare-track-id' support.
# 7/26/10 -- Added support for Ringtone licenses per FB 12173.
# 8/17/10 -- Added code to remove trailing space from track.title_clean
#      for EGR track searches.
# 11/3/10 -- Added validation check for fractional share
# 11/15/10 -- Removed fractional share validation check (these are ok!)
# 11/16/10 -- "All Digital" and "All Physical" are not valid product types.
# 11/29/10 -- Added share range check.
# 1/12/11 -- Added missing rateTypeID check
# 1/20/11 -- Added special handling for track lookups for VP (FB13163)
#  -- Added code to deal with Excel dates
# 2/15/11 -- Added check for "rate too small".
# 2/18/11 -- Added LP5 config to list of product types.
#  -- Changed existing license messaging for PD licenses
# 4/4/11 -- Added reserve total check
# 6/23/11 -- Added RT alias for Ring product type
# 6/29/11 -- Added rate% check
# 9/20/11 -- Updated ringtone validation
# 11/1/11 -- Additional RS fixes
# 11/4/11 -- Fixed percentOfSales logic found while testing 14736
# 12/20/11 -- Added lower bound check for rate percentage (a too small
#   value caused a run to die (Case 15279)).
# 2/23/12 -- Cleaned up productType with trailing spaces
# 3/23/12 -- Cleaned up rateBasis check
# 5/17/12 -- Fixed bug in findTrack (TODO: clean this function up)
# 8/30/12 -- Look for album title using both the title and/or title_clean
#   this is to accomodate clients whose clean_name isn't correct.
# 9/19/12 -- Removed upper bound check on rate percentage
# 9/20/12 -- Use clean_name_catalog() instead of clean()
# 1/2/13 -- Added validation logic to detect region errors
# 1/15/13 -- Updated validation for licenses w/ term data
# 1/16/13 -- Adjusted track search criteria in _findTrack
# 8/12/13 -- Added check for malformed share (FB608; shares had two decimal points)
# 1/27/14 -- Added Excel 2007 support
# 2/19/14 -- Added check for balance formatting; debug code for balance logic
# 2/9/15 -- License balances not working, plus SFW (Smithsonian) requires some
#          special logic to match their albums/tracks.
# 2/10/15 -- More album/track logic tweaking for SFW.  License balance should be ok.
# 3/26/15 -- Updated to allow MM-DD-YYYY (in addition to YYYY-MM-DD)
# 4/2/15 -- Added check to _validateHeader to see if the template is for this importer
# 5/7/15 -- Updated _normalizeDate logic to deal with Excel dates
# 6/11/15 -- Error out if the date isn't valid YYYY-MM-DD format
# 7/20/15 -- Accept additional rate type of 'S' for RING licenses
# 7/22/15 -- Accept additional rate type of 'F' for RING licenses
# 2/19/16 -- Remove CRLF from payor name
# 3/11/16 -- Remove CTRL-M from payor name
#  TODO: Need to store normalized dates in $rows, so that the dates look OK in the
#  exceptions report (especially when the dates are in Excel format)
#
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 qw(Add_Delta_Days Days_in_Month);

use Spreadsheet::ParseExcel;

use Common::Assert;
use Common::UTF8;
use Common::RSApp;
use Common::Util qw( clean_name_catalog trimspaces);

use RPS::License::BaseTrackLicense;
use RPS::License::US::PublicDomain;
use RPS::License::US::TrackLicense;
use RPS::License::US::RingtoneLicense;
use RPS::License::US::TrackLicenseWithLiquidationSchedule;

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::RegionCountryMap;
use RPS::DB::Item::Product;
use RPS::DB::Item::ProductType;
use RPS::DB::Item::PendingTransaction;
use RPS::Finance::PendingTransaction;
use RPS::DB::Item::FinanceAccount;
use RPS::Finance::Account;

use RPS::DB::Item::RegionCountryMap;

use Support::Implementation::ExcelReader;
use Support::Implementation::Excel2007Reader;
use Support::Implementation::TabDelimitedReader;
#use Support::Implementation::SearchUtil;

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 kNonUniqueAlbumName  => 103;
use constant kDuplicateTrackTitle => 104;
use constant kProductTypeNotFound => 99999;

binmode STDOUT, ":utf8";


my %gColumnMap; # Must define in subclass
my $dbo; # Must define in subclass
my $clientID; # Must define in subclass
my $execMode; # Must define in subclass

#sub new {
#   my ($class, %args) = @_;
#   my $self = bless {}, $class;
#   return $self->_init(%args);
#}


sub _init {
   my( $self, %args ) = @_;

   report("LicenseCommon -- args = ". Dumper(\%args));

   $self->SUPER::_init(%args);

   $execMode = $self->exec_mode if ( $self->exec_mode );

   return $self;
}

#--------------------------------------------------------------------------
# _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.  Exception message(s) will be placed in the error
# code column.  If a line is imported successfully, then the resulting
# licenseID will be stored in the import status column ("licenseID(###)"),
# otherwise this column will contain the string "__FAILED__".
#--------------------------------------------------------------------------
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};
      next if ( !$v ); # strip blank columns

      #----------------------------------------------------------
      # Do _not_ push the "Error Code" or "import-status" columns
      # if they were in the original template.  We'll re-create
      # them on the fly.
      #----------------------------------------------------------
      next if ( ("Error Code" eq $v) || ("import-status") eq $v );

      push @header, $v;
   }

   report("EXCEPTION:\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 $warningCode = $row->{'warning-code'};
      my $licenseID = $row->{'rs-license-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};
         next if ( !$v ); # strip blank columns
         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
         # these columns below.
         #----------------------------------------------------------
         next if ( ("Error Code" eq $v) || ("import-status") eq $v );

         push @obuf, $val;
      }

      my $importStatus;
      my $ecString;

      if ( $errorCode ) {
         $importStatus = "__FAIL__";
         $ecString = ($errorCode) ? $errorCode : '';
      } else {
         $importStatus = "license($licenseID)";
         $ecString = ($warningCode) ? $warningCode : '';
      }

      my $zzbuf = join("\t", "EXCEPTION:", @obuf, $ecString, $importStatus);

      report($zzbuf);
   }
}

#-------------------------------------------------------------
# _setReserve -- defines a ReserveLiquidation item if there is
# any percentage to liquidate
#-------------------------------------------------------------
sub _setReserve {
   my($period,$percent,$a) = @_;
   $percent = 0 if ( !$percent );
   $a->Period($period);
   $a->Percent($percent);
}

#-------------------------------------------------------------
# _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) = @_;

   #-------------------------------------------------------
   # 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 = (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/ ) {

         my $pArray = $v->getPendingTransactionArray();
         my $pType = ref $pArray;
         $hasError = $v->_hasError();
         my $errFlag = $hasError ? "*** ERROR ***" : '>> OK <<';

         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 <<';
            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();

         _dumpError( $leader . "      ",\%$v );

      } elsif( (ref $v) =~ /RPS::License::ReserveLiquidationList/ ) {

         my $reserveArray = $v->getList();
            
         printf("%s   %20s  %s\n%20s", $leader, $k, "@", $typeOf);
         for( my $i=0; $i<8; $i++ ) {
            printf("%s [%d] %d,", $leader, $i, $reserveArray->[$i]->Percent() );
         }
         print"\n";
      } else {
         my $errFlag = '';

         $v = "---" if ( !$v );
         printf("%s   %20s  %20s %s %s\n", $leader, $k, $v, $typeOf, $errFlag);
      }
   }
}# _dumpError

#---------------------------------------------------
# _isValidDateFmt - return true if the supplied date
# is in the format YYYY-MM-DD, false otherwise
#---------------------------------------------------
sub _isValidDateFmt {
   my($self, $dstr) = @_;

   my($yr,$mo,$dy) = split("-",$dstr);

   # Normally we're expecting YYYY-MM-DD

   my $st = 1; # valid unless we detect otherwise

   $st = 0 if ( !$yr || !$mo || !$dy );
   $st = 0 if ( $mo && ( $mo !~ /^\d+$/ ));
   $st = 0 if ( $yr && ( $yr !~ /^\d+$/ ));
   $st = 0 if ( $dy && ( $dy !~ /^\d+$/ ));
   $st = 0 if ( length($yr) != 4 );

   $st = 1 if( $dstr =~ /(\d{1,2})-(\d{1,2})-(\d{2})/ ); # MM-DD-YY

   if( $dstr =~ /(\d{4})-(\d{1,2})-(\d{1,2})/ ) # YYYY-MM-DD
   {
      return ( $3 > Days_in_Month($1, $2) ) ? 0 : 1; 
   }

   return $st;
}

# _normalizeDate: Convert a template date to a MySQL-complient date (YYYY-MM-DD)
#
sub _normalizeDate {
   my($self, $dstr) = @_;

   if( $dstr =~ /^\d{5}$/ ) {
      my $_dt = $self->numericToDate( $dstr );
      report("_normalizeDate:  Converted msft date $dstr to $_dt");
      $dstr = $_dt;
   }
   elsif( $dstr =~ /^(\d{1,2})-(\d{1,2})-(\d{2})$/ ) { # MM-DD-YY
      $dstr = sprintf("%04d-%02d-%02d", $3 + 2000, $1, $2);
   }
#   elsif( $dstr =~ /^(\d{4})[\-\\](\d{2})[\-\\](\d{2})$/ ) { # YYYY-MM-DD or YYYY/MM/DD
#   elsif( $dstr =~ /^(\d{4})-(\d{2})-(\d{2})$/ ||  # YYYY-MM-DD
#          $dstr =~ /^(\d{4})[\-\\](\d{2})[\-\\](\d{2})$/ )  YYYY/MM/DD
   elsif( $dstr =~ /^(\d{4})[-\/](\d{2})[-\/](\d{2})$/ ) { # YYYY-MM-DD or YYYY/MM/DD
      $dstr = sprintf("%04d-%02d-%02d", $1, $2, $3);
   }
   else {
      die("Unrecognized date format '$dstr'"); # XXX
   }
   return $dstr;
}

sub _isInteger {
   my($v) = @_;
   return ( $v and $v =~ m/^\d*$/ ) ? 1 : undef;
}

sub _findTrack {
   my(%args) = @_;
   my $trackID;
   my ($albumID,$errflag) = _findAlbum(%args);
   if ( ! $albumID ) {
      my $albumName = $args{album_name};
      my $catNo = $args{catalog_number} || "";
      report("   _findTrack: album($albumName) cat#($catNo) not found, skipping track");
      $errflag = kAlbumNotFound;
   } else {
      my $trackName = $args{track_name};
      $trackName =~ s/^\s+//; # remove leading spaces
      $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 $duplicate; # set if track title is not unique
      if ( !$isrc ) {
         # No ISRC; see if track title is a duplicate
         my $cleanTitle = clean_name_catalog($trackName);
         my $sql = "SELECT track_id FROM track "
            . "WHERE album_id = $albumID AND "
            #. "( title = ". $dbo->DBQuote($trackName) . " OR title_clean LIKE '$cleanTitle%' ) ";
            . "( title = ". $dbo->DBQuote($trackName) . " OR title_clean = '$cleanTitle' ) ";
         my $sth = $dbo->DoCmd($sql);
         if ( $sth->rows > 1 ) {
            report("_findTrack:DEBUG: duplicate title detected: sql = $sql");
            $errflag = kDuplicateTrackTitle;
            $duplicate = 1;
         }
      }

      if ( ! $duplicate ) {
         my $tColl = RPS::DB::Item::Track->GetTracksByAlbumID($albumID);
         while( $tColl->hasNext() ) {
            my $trackObj = $tColl->next();

            if ( $isrc ) {
               $isrc = trimspaces($isrc);
               my $masterID = $trackObj->master_id;
               my $mObj = RPS::DB::Item::Master->Lookup( master_id => $masterID );
               my $zzIsrc = $mObj->isrc;
               #if ( '' ne $isrc && $isrc eq $zzIsrc )

               my $t1 = lc clean_name_catalog($trackName);
               my $t2 = lc $trackObj->title_clean;

               my $actualTitle = $trackObj->title;

               # kludge for New World
               #$t2 =~ s/__/_/g if ( $clientID == 138 );

               $t2 =~ s/^_//g if ( $clientID == 290 ); # fearless

               # kludge for Nettwerk; remove trailing "_" from clean title
               $t2 =~ s/_$// if ( $clientID == 51 );

               # kludge for EGR; remove trailing "_" from clean title
               $t2 =~ s/_$// if ( $clientID == 183 );

               # kludge for VP Records; remove trailing "_" from clean title (FB13163)
               #$t2 =~ s/_$// if ( $clientID == 306 );
               if ( $clientID == 306 )
               {
                  $t1 =~ s/_$//;
                  $t2 =~ s/_$//;
               }

#               report("_findTrack:2: comparing titles [$t1] to [$t2], isrc [$isrc] to [$zzIsrc]");

               if( $clientID == 16 )  # XXX  SFW kludge
               {
                  report("_findTrack:SFW: comparing isrc [$isrc] to [$zzIsrc] - ignoring titles [$t1] to [$t2]");

                  # 2/11/15 - Now we're back to using ISRC and ignoring title ... oh eff me
                  # ignore track name, use ISRC for matching
#                  if( $isrc && $zzIsrc && '' ne $isrc && ($isrc eq $zzIsrc) )
#                  {
#                     $trackID = $trackObj->track_id;
#                     last;
#                  }
#                  # 2/10/15 - Now SFW wants to ignore ISRC and use the title ...
#                  # 2/12/15 - Now SFW wants to ignore ISRC and use the title ...
#
                  if( ($t1 eq $t2) || ( lc $actualTitle eq lc $trackName ))
                  {
                     $trackID = $trackObj->track_id;
                     last;
                  }
               }

               else
               {  # normal matching behavior for everyone else besides SFW

                  report("_findTrack:2: comparing titles [$t1] to [$t2], isrc [$isrc] to [$zzIsrc]");

                  # The ISRC and title must match
                  # If the RS track title matches but doesn't have an ISRC, then allow
                  # the match.
                  #
                  if ( $isrc && $zzIsrc && '' ne $isrc && ($isrc eq $zzIsrc) &&
                      (($t1 eq $t2) || ( lc $actualTitle eq lc $trackName )) )
                  {
                     $trackID = $trackObj->track_id;
                     last;
                  }
               }# XXX

            } else {
               my $t1 = lc clean_name_catalog($trackName);
               my $t2 = lc $trackObj->title_clean;

               # kludge for New World
               $t2 =~ s/__/_/g if ( $clientID == 138 );

               $t2 =~ s/^_//g if ( $clientID == 290 ); # fearless

               # kludge for Nettwerk; remove trailing "_" from clean title
               $t2 =~ s/_$//;

               # kludge for VP Records; remove trailing "_" from clean title (FB13163)
               $t2 =~ s/_$// if ( $clientID == 306 );

               report("_findTrack: comparing [$t1] to [$t2]");

               if ( $t1 eq $t2 ) {
                  $trackID = $trackObj->track_id;
                  last;
               }
            }
         }#track loop

         $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 && $clientID != 16 ) {
      my %a = (
         title => $albumName,
      );
      $a{catalog_number} = $catNo if ( $catNo );
      #report("   _findAlbum: looking for CAT#($catNo) title($albumName)");

      my $cleanAlbumTitle = clean_name_catalog($albumName);
      report("   _findAlbum: looking for CAT#($catNo) title($albumName) titleClean($cleanAlbumTitle)");

      #-------------------------------------------------------------------
      # Warning: if a title has a trailing space in the database, then the
      # clean version will have a trailing underscore.  We use LIKE to try
      # and prevent this from not finding the album.
      #-------------------------------------------------------------------

      #my $sql = "SELECT album.* FROM album WHERE title_clean LIKE '$cleanAlbumTitle%' ";

      #my $sql = "SELECT album.* FROM album WHERE "
      #   . "( title = ". $dbo->DBQuote($albumName) . " OR title_clean LIKE '$cleanAlbumTitle%' ) ";

      my $sql = "SELECT album.* FROM album WHERE "
         . "( title = ". $dbo->DBQuote($albumName) . " OR title_clean = '$cleanAlbumTitle' ) ";
      $sql .= " AND catalog_number = " . $dbo->DBQuote($catNo) if ( $catNo );

report("D: _findAlbum: sql = $sql"); # XXX XXX
      my $albumColl = RPS::DB::Item::Album->GetAll( $sql );

      $errflag = kAlbumNotFound if ( $albumColl->size() == 0 );
      $errflag = kNonUniqueAlbumName if ( $albumColl->size() > 1 );

      if ( !$errflag ) {
         my $aObj = $albumColl->next();
         $albumID = $aObj->album_id;
         report("   _findAlbum: found albumID($albumID)");
      }
   }
   elsif( $clientID == 16 )   # XXX XXX SFW kludge - ignore album name, search using catno
   {
      assert($catNo);
      my $sql = "SELECT album.* FROM album WHERE "
         . "catalog_number = " . $dbo->DBQuote($catNo);

      my $albumColl = RPS::DB::Item::Album->GetAll( $sql );

      $errflag = kAlbumNotFound if ( $albumColl->size() == 0 );
      $errflag = kNonUniqueAlbumName if ( $albumColl->size() > 1 );

      if ( !$errflag ) {
         my $aObj = $albumColl->next();
         $albumID = $aObj->album_id;
         report("   _findAlbum_SFW: found albumID($albumID)");
      }
   }
   return ($albumID, $errflag);
}# _findAlbum

sub _findPublisher {
   die("Must override");
}

sub _findPayor {
   my($self, %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($self, %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;
}

#--------------------------------------------------------
# For 'ALL' or 'D' licenses, the region must be US or CA.
# This routine checks the specified region and sees if it
# contains any other countries besides US and/or CA
#--------------------------------------------------------
sub _validDigitalRegion {
   my($regionID) = @_;
   my $valid = 1;
   my $coll = RPS::DB::Item::RegionCountryMap->GetByRegionID($regionID);
   while($valid && $coll->hasNext() ) {
      my $rcObj = $coll->next();
      my $cc = uc $rcObj->country_code;
      $valid = 0 if ( $cc ne 'US' && $cc ne 'CA' );
   }
   return $valid;

}
#-----------------------------------------------------------
# _getCrossedID -- returns a value that can be used to set a
# license's cross_collateralized field
#-----------------------------------------------------------
sub _getCrossedID {
   my($self, %args) = @_;
   my $crossed = $args{crossed};
   my $crossedID = 0; # no - default
   if ( $crossed && $crossed =~ /^y/i ) {
      $crossedID = 1; 
   }
   return $crossedID;
}

#----------------------------------------------------------
# _getStatusID -- returns a value that can be used to set a
# license's 'inactive' field
#----------------------------------------------------------
sub _getStatusID {
   my($self, %args) = @_;
   my $status = $args{inactive};
   my $statusID = 0; # active - default
   if ( $status && $status =~ /^i/i ) {
      $statusID = 1; 
   }
   return $statusID;
}

#----------------------------------------------------------
# _getPublisherDirect -- returns a value that can be used
# to set a license's 'publisher_direct' field.
#----------------------------------------------------------
sub _getPublisherDirect {
   my($self, %args) = @_;
   my $pdflag = $args{publisher_direct};
   my $statusID = 0; # default is "not publisher direct"
   if ( $pdflag && $pdflag =~ /^y/i ) {
      $statusID = 1;  # publisher direct
   }
   return $statusID;
}

#--------------------------------------------------------------
# _getRateBasis -- returns a rate basis identifier based on the
# supplied text string
#--------------------------------------------------------------
sub _getRateBasis {
   die("Must override");
}

#-----------------------------------------------------------------
# _getRateType -- returns an RPS rate type identifier based on the
# supplied rate type string.
# If a rate type isn't found, then an undefined rate type ID will
# be returned to the caller.
#-----------------------------------------------------------------
sub _getRateType {
   die("Must override");
}

sub _getProductTypeID {
   my($self, %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 254 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$/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$/i );
   return RPS::DB::Item::Product::kProductTypeLP5 if ( $cfg =~ /^lp5$/i );
   return kProductTypeNotFound;
}

sub _appendString {
   my($self, $str,$v) = @_;

   if ( $str ) {
      my $cur = $str;
      my $newstring = "$cur; $v";
      $_[0] = $newstring;
   } else {
      $_[0] = $v;
   }
   return;
die("str($str) v($v)") if ( !$str );

   my $cur = $str;
   my $prefix = "$cur; $v";
   $_[0] = ($str) ? $prefix : $v;
}# _appendString

sub _getError {
   my($self, $licensehash, $errormap) = @_;
   #-------------------------------------------------------
   # 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 %$licensehash ) {

      my $v = $licensehash->{$k};
      my $typeOf = ref $v;

      my $hasError = '';

      if ( (ref $v) =~ /Common::FormObject::Scalar/ ||
           (ref $v) eq "Common::FormObject::DateTime" ) {

         if ( $v->_hasError() ) {
            my %xmlParams = $v->getXMLParams();
            my $msg = defined $xmlParams{emsg} ? $xmlParams{emsg} : "MSG_NOT_AVAILABLE";
            my $e = $xmlParams{e};
            $errormap->{$k} = $msg;
         }
      }
   }

}# _getError2

sub _reportError {
   my ($name, $obj) = @_;

   report("   _reportError: checking '$name' for errors");
   if ($obj && $obj->_hasError()) {

      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";
   }
}

sub _numberTooSmall {
   my($n) = @_;
   #return if ( $n );
   return ($n && ($n < 0.0001)) ? 1 : 0;
}

sub numericToDate
{
   #my $self = shift;
   my($self, $ndays ) = @_;

   ## Excel date is (supposedly) days since 01 Jan 1900, but have to subtract 2
   ## from that number because (1) 0/1 index issue, and (2) MS wants to pretend
   ## that 1900 was a leap year.
   my @date = Add_Delta_Days(1900, 1, 1, $ndays - 2);
   return wantarray ? @date : sprintf("%d-%02d-%02d", @date);
}

1;
