package Import::Date;

use strict;
use lib '/app/tools/common/lib';
use Common::Log;
use Common::Assert;

use Date::Calc qw(Add_Delta_Days Days_in_Month Decode_Month);

# It would be nice to put our date parsing into a class structure, but for
# now we will just encapsulate it in a package

sub new {
    my $class = shift;
    my %args  = @_;

    my $self = bless {}, $class;
    return $self->_init(%args);
}

sub _init {
    my ( $self, %args ) = @_;

    return $self;
}

#------------------------------------------------------------
# str array GetDates
#
# Converts strings containing begin/end dates from a sale file
# in one format to strings that are in the ISO 8601 date
# format, for importing sales files. Days are ignored (for now).
# The date begin and date end that are returned are the first
# day of the month of the 'date_begin' year, and the last day
# of the month of the 'date_end' year, where the 'date_end'
# parameter is set to the 'date_begin' parameter if no
# 'date_end' parameter is passed in.
# Example:
# Let date_begin = 12/31/2009
# Let date_end   = 01/01/2010
# return (2009-12-01, 2010-01-31)
# This method is extensible
# to allow for new patterns to be inserted as they are
# discovered in particular file formats. See the inline
# comments on how this is achieved.
#
# Parameters:
#   str date_begin (required) - string containing the
#   beginning date of the sale file/record.
#
#   str date_end (optional) - string containing the end
#   date of the sale file/record.
#
#   str type (optional) - string which indicates the type
#   of date corresponding to the different types in the
#   'datePatterns' hash. When this type is passed in only
#   date patterns that correspond to that type are tried
#   for a match.
#   -----------IMPORTANT---------
#   Dates passed in, in the format xx/xx/xx(xx) are by default
#   parsed as dates in the U.S. date format: mm/dd/yy(yy).
#   In order to parse them as in the European format
#   dd/mm/yy(yy) you must pass in the type 'eur' as the
#   type parameter.
#   -----------IMPORTANT---------
#
# Return:
# (str iso_date_begin(yyyy-mm-dd), str iso_date_end(yyyy-mm-dd))
#
# test on the cmd line (from BookPub::Import) - add the 'type' arg as needed
# perl -MImporter -e "print join(' - ', BookPub::Import::Importer->_getDates(date_begin=>'2009-06-01'))"
#------------------------------------------------------------
sub GetDates {
    my %args = @_;

    my $dateBegin = lc $args{date_begin};
    my $dateEnd   = lc $args{date_end};
    my $types     = $args{type};

    # Going to allow type to be an array ref of 'types' to try.
    # But we still want to support just passing in a single type.
    # So convert 'type' to an array if it isn't already.
    #
    if ( $types && !ref($types) ) {
        $types = [$types];
    }

    #Normalized month and year values: mm, yy, yyyy with digits 0-9
    my $dayBeginVal;
    my $monthBeginVal;
    my $yearBeginVal;

    my $dayEndVal;
    my $monthEndVal;
    my $yearEndVal;

    #Lookup hashes for raw extracted date values
    my %textMonthVals = (
        "jan" => 1,
        "feb" => 2,
        "mar" => 3,
        "apr" => 4,
        "may" => 5,
        "jun" => 6,
        "jul" => 7,
        "aug" => 8,
        "sep" => 9,
        "oct" => 10,
        "nov" => 11,
        "dec" => 12
    );

    my %frenchTextMonthVals = (
        "janvier"   => 1,
        "janv"      => 1,
        "fevrier"   => 2,
        "février"   => 2,
        "fevr"      => 2,
        "mars"      => 3,
        "avril"     => 4,
        "mai"       => 5,
        "juin"      => 6,
        "juillet"   => 7,
        "juil"      => 7,
        "aout"      => 8,
        "août"      => 8,
        "septembre" => 9,
        "sept"      => 9,
        "octobre"   => 10,
        "oct"       => 10,
        "novembre"  => 11,
        "nov"       => 11,
        "decembre"  => 12,
        "décembre"  => 12,
        "dec"       => 12
    );

    my %quarterBeginMonthVals = (
        1 => 1,
        2 => 4,
        3 => 7,
        4 => 10
    );

    my %quarterEndMonthVals = (
        1 => 3,
        2 => 6,
        3 => 9,
        4 => 12
    );

    #Date type 'text'
    my $textSub = sub {
        my $m1 = substr( $_[0], 0, 3 ) if $_[0];
        my $y1 = $_[1] if $_[1];
        my $m2 = substr( $_[2], 0, 3 ) if $_[2];
        my $y2 = $_[3] if $_[3];

        #Convert months in text form to digit form (0-12)
        return ( $textMonthVals{$m1}, $y1, $textMonthVals{$m2}, $y2 )
          if ( $m1 && $y1 && $m2 && $y2 );

        return ( $textMonthVals{$m1}, $y1 )
          if ( $m1 && $y1 );

        return undef;
    };

    my $frenchTextSub = sub {

        #Convert months in text form to digit form (0-12)
        return ( $frenchTextMonthVals{ $_[0] }, $_[1], $frenchTextMonthVals{ $_[2] }, $_[3] )
          if ( $_[0] && $_[1] && $_[2] && $_[3] );

        return ( $frenchTextMonthVals{ $_[0] }, $_[1] )
          if ( $_[0] && $_[1] );

        return undef;
    };

    #Date type 'quarter'
    my $quarterSub = sub {

        #If two quarters/years take first year and first month from first
        # quarter/year and last year and last month of the quarter
        # from second quarter/year. If one quarter/year take first/last month
        # from quarter and year from year.
        if ( $_[0] && $_[1] && $_[2] && $_[3] ) {
            return ( $quarterBeginMonthVals{ $_[0] }, $_[1], $quarterEndMonthVals{ $_[2] }, $_[3] );
        } elsif ( $_[0] && $_[1] ) {
            return ( $quarterBeginMonthVals{ $_[0] }, $_[1], $quarterEndMonthVals{ $_[0] }, $_[1] );
        }

        return undef;
    };

    #Date type 'msft'
    my $msftSub = sub {

        #Convert date to mm yy (0-9) by adding days passed in
        #to jan 1st 1900
        my @begin_date = Add_Delta_Days( 1900, 1, 1, $_[0] );

        my @end_date;

        if ( $_[2] ) {
            @end_date = Add_Delta_Days( 1900, 1, 1, $_[2] );
        }

        my ( $begin_year, $begin_month, $begin_day ) = @begin_date;
        my ( $end_year,   $end_month,   $end_day )   = @end_date;

        return ( $begin_month, $begin_year, $end_month, $end_year );
    };

    #Year patterns used in combination with date patterns: 2009 | 09
    my $yearPatterns = [ '(\d{4})', '(\d{2})' ];

    #@datePatterns is the array that contains all of the date patterns
    #that are matched against the 'date_begin' and 'date_end' parameters.
    # -------------IMPORTANT----------
    # ORDER MATTERS!!
    # The date patterns and all patterns in general are searched in ORDER.
    # This means that the U.S. date format will always be hit before the
    # european date format because the patterns are the same, but the rules
    # are different. If european date formats are needed, the 'eur' type
    # must be passed in explicitly.
    # --------------IMPORTANT---------
    #
    #This array contains string/array ref pairs that correspond to each
    #date type. Each array reference contains array ref/sub ref pairs.
    #The array ref contains strings of regular expressions which correspond
    #to the patterns that match dates of that type, and a corresponding
    #hash ref that has the strings 'm' and 'y' pointing to values
    #(1 through 3). This hash ref is the 'rule' that goes with the pattern.
    #Note that if no rule is declared then whatever is matched in the first
    #group of the regular expression is returned for all date values
    #(month_begin, year_begin), see the type 'msft' as an example of this.
    #The strings 'm' and 'y' represent month and year
    #respectively. And the values they point to indicate which group of
    #the regular expression they are extracted from. As an example,
    #the 'us' style pattern extracts month from the first group (\d{2})
    #and year from the second group (\d{4})). However, these numerical values
    #can be substituted for array refs that contain patterns, in order to
    #generalize certain patterns and have them more 'free form.' See the
    #'quarter' pattern as an example. in the 'quarter' pattern, the year
    #part of the hash ref is set to '$yearPatterns' which has regular
    #expressions ['(\d{4})','(\d{2})']. This means that once the initial
    #pattern match is complete, a search for the year begins with the pattern
    #'(\d{4})' and ends with the pattern (\d{2}). This cuts down on the
    #number of patterns which must be delcared explicitly for each type.
    #    Since each pattern extracts raw strings corresponding to months/years
    #an optional sub reference can be declared for each pattern/sub ref
    #pair. This subroutine is called to convert the raw dates extracted
    #from the date string to normalized date values mm yy with values 0-9.
    #After the raw data is extracted using the pattern, it is passed in
    #to the subroutine in the format:
    #(raw_month_begin, raw_year_begin, raw_month_end, raw_year_end), with
    #'raw_month_end' and 'raw_year_end' being optional. The subroutine returns
    #the normalized dates in the same order they were passed in.
    # If no sub ref is declared, then the raw values are assumed to be in the
    #normalized format already and will be returned as such.
    my %datePatterns = (

        # JPK - Going to give the ISO format the first crack at it.
        # Otherwise, the US patterns seem inclined to take it and screw it up...
        #
        #2008-01-01 20080101 (ISO format or format similar to ISO)
        'iso' => [ [
                '(\d{4})\/(\d{2})\/(\d{2})' => { 'y' => 1, 'm' => 2, 'd' => 3 },
                '(\d{4})\-(\d{2})\-(\d{2})' => { 'y' => 1, 'm' => 2, 'd' => 3 },
                '(\d{4})(\d{2})(\d{2})'     => { 'y' => 1, 'm' => 2, 'd' => 3 }
            ],
            undef
        ],

        #US style dates: 12/31/2008 | 12/31/08
        #pattern                   #rule
        'us' => [ [
                '(\d{2})\D(\d{2})\D(\d{4})' => { 'm' => 1, 'd' => 2, 'y' => 3 },    #sub ref
                '(\d{2})\D(\d{2})\D(\d{2})' => { 'm' => 1, 'd' => 2, 'y' => 3 },
                '(\d+)\D(\d+)\D(\d{4})'     => { 'm' => 1, 'd' => 2, 'y' => 3 },
                '(\d+)\D(\d+)\D(\d{2})'     => { 'm' => 1, 'd' => 2, 'y' => 3 },
                '(\d{2})(\d{2})(\d{4})'     => { 'm' => 1, 'd' => 2, 'y' => 3 }
            ],
            undef
        ],

        #                   '\d{2}\D(\d{2})\D(\d{2})'=>{'m'=>1,'y'=>2}], undef],
        'eur' => [ [
                '(\d+)\/(\d+)\/(\d+)'   => { 'd' => 1, 'm' => 2, 'y' => 3 },
                '(\d+)\-(\d+)\-(\d+)'   => { 'd' => 1, 'm' => 2, 'y' => 3 },
                '(\d{2})(\d{2})(\d{4})' => { 'd' => 1, 'm' => 2, 'y' => 3 },
            ],
            undef
        ],

        #jan 09 | 09 jan | jan 2009 | 2009 jan
        'text' =>
          [ [ '(jan|feb|mar|' . 'apr|may|jun|' . 'jul|aug|sep|' . 'oct|nov|dec)' => { 'm' => 1, 'y' => $yearPatterns } ], $textSub ],

        #janv 09 | 09 jan | jan 2009 | 2009 jan
        'french_text' => [ [
                    '(janvier|fevrier|'
                  . 'mars|avril|mai|juin|'
                  . 'juillet|aout|septembre|'
                  . 'octobre|novembre|decembre|'
                  . 'février|août|décembre|janv|'
                  . 'fevr|juil|sept|oct|nov|dec)' => { 'm' => 1, 'y' => $yearPatterns }
            ],
            $frenchTextSub
        ],

        #q1 2008 | 1q 2008 | 20081q | 2008q1
        'quarter' => [ [
                'q(\d)' => { 'm' => 1, 'y' => $yearPatterns },
                '(\d)q' => { 'm' => 1, 'y' => $yearPatterns }
            ],
            $quarterSub
        ],

        #09-2008 09/2008
        'numerical' => [ [
                '(\d+)\D(\d{4})' => { 'y' => 2, 'm' => 1 },
                '(\d{2})(\d{2})' => { 'y' => 2, 'm' => 1 }
            ],
            undef
        ],

        #2008-09 | 2008/09 | 2011/9 | 20080901 | 200809 | 0809
        'numerical_year_first' => [ [
                '(\d{4})\D(\d{1,2})'  => { 'y' => 1, 'm' => 2 },
                '(\d{4})(\d{2})\d{2}' => { 'y' => 1, 'm' => 2 },
                '(\d{4})(\d{2})'      => { 'y' => 1, 'm' => 2 },
                '(\d{2})(\d{2})'      => { 'y' => 1, 'm' => 2 }
            ],
            undef
        ],

        #Microsoft date format: 34519
        'msft' => [ [ '(\d+)' => undef ], $msftSub ],
    );

    # This is the default order of patterns to try.
    #
    my @defaultPatternOrder = ( 'iso', 'us', 'eur', 'text', 'french_text', 'quarter', 'numerical', 'numerical_year_first', 'msft', );

    #    my %datePatternsHash = @datePatterns;

    #Raw dates extracted from regular expressions defined by date patterns
    my $rawDayBegin;
    my $rawMonthBegin;
    my $rawYearBegin;

    my $rawDayEnd;
    my $rawMonthEnd;
    my $rawYearEnd;

    # If the type is passed in, only iterate through the patterns of that
    # date type, otherwise, iterate through all patterns in order of appearence
    # in the array

    # JPK - Why have two version of such similar logic?
    # Instead, if they did not pass in a type (or a list of types), we'll push all the
    # types into a list, and _always_ iterate over the list.
    #
    if ( !defined $types ) {
        $types = \@defaultPatternOrder;
    }

    foreach my $type (@$types) {
        my $patternSet = $datePatterns{$type};

        my $patterns = $patternSet->[0];
        my $typeSub  = $patternSet->[1];

        ( $rawDayBegin, $rawMonthBegin, $rawYearBegin ) = _matchExtractDate( $dateBegin, $patterns );

        if ( $rawMonthBegin && $rawYearBegin ) {
            if ($dateEnd) {
                ( $rawDayEnd, $rawMonthEnd, $rawYearEnd ) = _matchExtractDate( $dateEnd, $patterns );
            }

            if ($typeSub) {
                ( $monthBeginVal, $yearBeginVal, $monthEndVal, $yearEndVal ) =

                  &$typeSub( $rawMonthBegin, $rawYearBegin, $rawMonthEnd, $rawYearEnd );
                $dayBeginVal = $rawDayBegin;
                $dayEndVal   = $rawDayEnd;
            } else {
                ( $dayBeginVal, $monthBeginVal, $yearBeginVal, $dayEndVal, $monthEndVal, $yearEndVal ) =
                  ( $rawDayBegin, $rawMonthBegin, $rawYearBegin, $rawDayEnd, $rawMonthEnd, $rawYearEnd );
            }

            last;
        }    # end if
    }

    #Convert normalized dates to ISO format and return
    if (   $yearBeginVal
        && ( !$dateEnd || $yearEndVal )
        && $monthBeginVal
        && ( !$dateEnd || $monthEndVal ) ) {
        if ( $yearBeginVal < 1000 ) {
            $yearBeginVal += 2000;
        }

        if ( $yearEndVal < 1000 ) {
            $yearEndVal += 2000;
        }

        if ( !$dateEnd && !( $yearEndVal && $monthEndVal ) ) {
            $yearEndVal  = $yearBeginVal;
            $monthEndVal = $monthBeginVal;
            $dayEndVal   = $dayBeginVal;
        }

        if ( !$dayBeginVal ) {
            $dayBeginVal = 1;
        }

        if ( !$dayEndVal ) {
            $dayEndVal = Days_in_Month( $yearEndVal, $monthEndVal );
        }

        my $isoBegin = sprintf( "%04d-%02d-%02d", $yearBeginVal, $monthBeginVal, $dayBeginVal );
        my $isoEnd   = sprintf( "%04d-%02d-%02d", $yearEndVal,   $monthEndVal,   $dayEndVal );

        # Use this util function to choose the best month, if appropriate.
        #
        return Common::Util::normalize_sales_month_dates( $isoBegin, $isoEnd );

        #Always return dates in the ISO format: yyyy-mm-dd with the day always
        #going from the first to the last day of the month in 'month_end_val'
        #        return (
        #        sprintf("%04d-%02d-01", $yearBeginVal, $monthBeginVal),
        #        sprintf("%04d-%02d-%02d", $yearEndVal, $monthEndVal,
        #        Days_in_Month($yearEndVal, $monthEndVal))
        #        );
    }

}

#------------------------------------------------------------
# str array _matchExtractDate
#
# This subroutine takes in a date of an unknown type and tries
# to match it to an array of patterns that are passed in. Then
# it extracts relevant date information (month and year) according
# to a corresponding rule. Then it returns the raw data extracted.
#
# Parameters:
#   str $date (required) - A date in some unknown format that must be
#   matched to a pattern of a certain date type.
#
#   array reference $typePatterns - Reference to an array containing
#   pattern/rule pairs
#
# Return:
# ( str month, str year)
#------------------------------------------------------------
sub _matchExtractDate {
    my $date         = shift;
    my $typePatterns = shift;

    my $day = 1;
    my $month;
    my $year;

    my @typePatternsArray = @$typePatterns;

    my $typePatternsArraySize = @typePatternsArray;

    #Iterate through the pattern/rule pairs
    $typePatternsArraySize /= 2;

    Common::Log::Debug("Extracting date from '$date'");
    for ( 1 .. $typePatternsArraySize ) {
        my $typePattern        = shift @typePatternsArray;
        my $typePatternRuleRef = shift @typePatternsArray;

        Common::Log::Debug("Evaluting this rule: $typePattern");
        if ( $date =~ /$typePattern/i ) {
            Common::Log::Debug(" - Matches");

            # If there is no rule defined for the pattern, return whatever was
            # matched
            if ( !$typePatternRuleRef ) {
                my $retDates = $1;
                return ( $retDates, $retDates, $retDates );
            }

            my %typePatternRuleHash = %$typePatternRuleRef;

            # Get the rules for the location of the month and the year
            # !!! and day!
            my $dayRule   = $typePatternRuleHash{'d'};
            my $monthRule = $typePatternRuleHash{'m'};
            my $yearRule  = $typePatternRuleHash{'y'};

            # There may not _be_ a day rule.
            #
            if ($dayRule) {

                #If the rule is an array, iterate through the patterns,
                #extract the day and then remove the day portion from the
                #date
                if ( ref($dayRule) eq 'ARRAY' ) {
                    my @dayPatterns = @{$dayRule};

                    foreach my $dayPattern (@dayPatterns) {
                        if ( $date =~ /$dayPattern/i ) {
                            $day = $1;
                            $date =~ s/$dayPattern//g;
                            last;
                        }
                    }
                } else {

                    #The value of the rule tells us which group the day is in
                    #the regular expression
                    if ( $dayRule == 1 ) {
                        $day = $1;
                    } elsif ( $dayRule == 2 ) {
                        $day = $2;
                    } elsif ( $dayRule == 3 ) {
                        $day = $3;
                    }

                }
            }

            #If the month rule is an array, iterate through the patterns,
            #extract the month and then remove the month portion from the
            #date
            if ( ref($monthRule) eq 'ARRAY' ) {
                my @monthPatterns = @{$monthRule};

                foreach my $monthPattern (@monthPatterns) {
                    if ( $date =~ /$monthPattern/i ) {
                        $month = $1;
                        $date =~ s/$monthPattern//g;
                        last;
                    }
                }
            } else {

                #The value of the rule tells us which group the month is in
                #the regular expression
                if ( $monthRule == 1 ) {
                    $month = $1;
                } elsif ( $monthRule == 2 ) {
                    $month = $2;
                } elsif ( $monthRule == 3 ) {
                    $month = $3;
                }

            }

            #Same procedure for year as month
            if ( ref($yearRule) eq 'ARRAY' ) {
                my @yearPatterns = @{$yearRule};
                $date =~ s/$typePattern//g;
                foreach my $yearPattern (@yearPatterns) {
                    if ( $date =~ /$yearPattern/i ) {
                        $year = $1;
                        $date =~ s/$yearPattern//g;
                        last;
                    }
                }
            } else {
                if ( $yearRule == 1 ) {
                    $year = $1;
                } elsif ( $yearRule == 2 ) {
                    $year = $2;
                } elsif ( $yearRule == 3 ) {
                    $year = $3;
                }
            }

            if ( $day && $month && $year ) {
                return ( $day, $month, $year );
            }

        }
    }

    return undef;
}

1;
