#------------------------------------------------------------
# Copyright (C) 2008 RoyaltyShare, Inc.   All Rights Reserved
#------------------------------------------------------------
package Common::DateTimeFormat;

use lib '/app/tools/common/lib';
use Common::Assert;
use Common::XMLObject;

use base 'Common::XMLObject';

# We'll have some flags we can use to 'tune' output.
# !!! For now, there is just 1 flag:  Whether to
# !!! show the 'thousands' separator or not.
#
# This hash defines both what flags are available, and what their default
# settings are.
#
my %gFlags = ( fourDigitYearFlag => 1, );

# We need to define:
# The date separator character.
# The date order.
# I'll just combine them into a signel 'dateFormat' string, like this:
# dateFormat => 'M/D/Y'.
# Then I can just use a regex to replace M,D,and Y with the correct values.
#
# For times, h == 12 hour format, H == 24 hour format, m == minutes, s == seconds.
# If there is a 'A', that equates to the 'AM' or 'PM' symbol.
# ... Which will also need to be defined

# Keyed by ISO 639-1 language code and country code
#
my %gDefs = (
    'elGR' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'H:m:s' },

    'enAU' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'h:m:s A', am => 'AM',   pm => 'PM' },
    'enBG' => { dateSeparator => '.', dateFormat => 'D.M.Y', timeFormat => 'H:m:s' },
    'enBR' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'H:m:s' },
    'enCA' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'h:m:s A', am => 'AM',   pm => 'PM' },
    'enGB' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'H:m:s' },
    'enDK' => { dateSeparator => '-', dateFormat => 'Y-M-D', timeFormat => 'H:m:s' },
    'enEU' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'H:m:s' },
    'enHK' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'H:m:s' },
    'enZA' => { dateSeparator => '/', dateFormat => 'Y/M/D', timeFormat => 'h:m:s',   am => 'AM',   pm => 'PM' },
    'enJM' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'h:m:s A', am => 'AM',   pm => 'PM' },
    'enNO' => { dateSeparator => '.', dateFormat => 'D.M.Y', timeFormat => 'H:m:s' },
    'enNZ' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'h:m:s A', am => 'a.m.', pm => 'p.m.' },
    'enSE' => { dateSeparator => '-', dateFormat => 'Y-M-D', timeFormat => 'H:m:s' },
    'enUS' => { dateSeparator => '/', dateFormat => 'M/D/Y', timeFormat => 'h:m:s A', am => 'AM',   pm => 'PM' },
    'enKR' => { dateSeparator => '-', dateFormat => 'Y-M-D', timeFormat => 'H:m:s' },

    'esES' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'H:m:s' },
    'esMX' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'h:m:s A', am => 'a.m.', pm => 'p.m.' },

    'frFR' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'H:m:s' },
    'frCH' => { dateSeparator => '.', dateFormat => 'D.M.Y', timeFormat => 'H:m:s' },

    'grDE' => { dateSeparator => '.', dateFormat => 'D.M.Y', timeFormat => 'H:m:s' },
    'grCH' => { dateSeparator => '.', dateFormat => 'D.M.Y', timeFormat => 'H:m:s' },

    'itIT' => { dateSeparator => '/', dateFormat => 'D/M/Y', timeFormat => 'H.m.s' },
    'itCH' => { dateSeparator => '.', dateFormat => 'D.M.Y', timeFormat => 'H:m:s' },

    'isIS' => { dateSeparator => '.', dateFormat => 'D.M.Y', timeFormat => 'H:m:s' },

    'nlNL' => { dateSeparator => '-', dateFormat => 'D-M-Y', timeFormat => 'H:m:s' },

    'jpJP' => { dateSeparator => '/', dateFormat => 'Y/M/D', timeFormat => 'H:m:s' },

    'zhCN' => { dateSeparator => '/', dateFormat => 'Y/M/D', timeFormat => 'H:m:s' },
);

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

    assert( $args{languageCode}, "ERROR - you must specify a languageCode" );
    assert( $args{countryCode},  "ERROR - you must specify a countryCode" );

    my $languageCode = $args{languageCode};
    my $countryCode  = $args{countryCode};
    my $pair         = "$languageCode$countryCode";
    my $def          = $gDefs{$pair} || die "ERROR - unknown combo '$pair'";

    # !!! Will I need to break this out further?
    #
    $self->{DateSeparator} = $def->{dateSeparator};
    $self->{DateFormat}    = $def->{dateFormat};
    $self->{TimeFormat}    = $def->{timeFormat};
    $self->{AM}            = $def->{am} if defined $def->{am};
    $self->{PM}            = $def->{pm} if defined $def->{pm};

    return $self;
}

# This method is used to _temporarily_ override the default formatting settings.
# It returns an object reference - As long as this object remains in scope,
# your new settings will remain in effect.  As soon as it goes _out_ of scope,
# the original settings will revert.
#
# You can in theory nest calls to this method if you want to.
#
sub TemporarySettings {
    my (%flags) = @_;

    my $reverter = Common::DateTimeFormat::SettingsReverter->new( flags => \%gFlags );

    # !!! Sanity-check the flags args.
    #
    foreach my $inFlag ( keys %flags ) {
        assert( defined $gFlags{$inFlag}, "ERROR - unknown flag $inFlag" );
        $gFlags{$inFlag} = $flags{$inFlag};
    }

    return $reverter;
}

sub dateSeparator {
    my ($self) = @_;
    return $self->{DateSeparator};
}

sub dateFormat {
    my ($self) = @_;
    return $self->{DateFormat};
}

sub timeFormat {
    my ($self) = @_;
    return $self->{TimeFormat};
}

sub formatDateTime {
    my ( $self, $datetime ) = @_;
    assert($datetime);

    my ( $date, $time ) = split( / /, $datetime );

    return $self->formatDate($date) . ' ' . $self->formatTime($time);
}

sub formatDate {
    my ( $self, $date ) = @_;

    assert($date);

    # To make this a bit easier to use, we'll split out the time (if there is one)
    #
    my ( $datePart, $timePart ) = split( / /, $date );
    if ( defined $timePart ) {
        $date = $datePart;
    }

    # Incoming dates will always be in our internal database format, which
    # is 'YYYY-MM-DD'
    #

    my ( $year, $month, $day ) = split /-/, $date;

    #    die "ERROR - date param '$date' does not appear to have a valid year" unless (defined $year && $year > 0);
    #    die "ERROR - date param '$date' does not appear to have a valid month" unless (defined $month && $month >= 1 && $month <= 12);
    #    die "ERROR - date param '$date' does not appear to have a valid day" unless (defined $day && $day >= 1 && $day <= 31);

    # Chop the year down to 2 digits if requested.
    #
    if ( !$gFlags{fourDigitYearFlag} ) {
        $year = substr( $year, -2, 2 );
    }

    my $outputString = $self->{DateFormat};
    $outputString =~ s/Y/$year/;
    $outputString =~ s/M/$month/;
    $outputString =~ s/D/$day/;

    return $outputString;
}

sub formatTime {
    my ( $self, $time ) = @_;
    assert($time);

    # Incoming times will always be in our internal database format: HH:MM:SS (hours are 24 hour)
    #
    my ( $hours, $minutes, $seconds ) = split( /:/, $time );

 #    die "ERROR - time param '$time' does not appear to have a valid hour" unless (defined $hours && $hours >= 0 && $hours <= 23);
 #    die "ERROR - time param '$time' does not appear to have a valid minutes" unless (defined $minutes && $minutes >= 0 && $minutes <= 59);
 #    die "ERROR - time param '$time' does not appear to have a valid seconds" unless (defined $seconds && $seconds >= 0 && $seconds <= 59);

    # Figure out what the 12-hour version of the hour value ought to be.
    #
    my $twelveHours = ( $hours > 12 ? $hours - 12 : $hours );

    my $ampm = ( $hours > 11 ? $self->{PM} : $self->{AM} );

    my $outputString = $self->{TimeFormat};
    $outputString =~ s/H/$hours/;
    $outputString =~ s/h/$twelveHours/;
    $outputString =~ s/m/$minutes/;
    $outputString =~ s/s/$seconds/;
    $outputString =~ s/A/$ampm/;

    return $outputString;
}

# This method will attempt to parse an incoming string into our internal YYYY-MM-DD format,
# using the locale settings as a hint.
#
sub parseDate {
    my ( $self, $string ) = @_;

    assert($string);

    # Figure out whether we're expecting dates in 'european', 'us', or 'asian' form.
    # We can leverage the power of Date::Calc for european and us dates.
    # Asian... well, we will do our best.
    #
    my $sep = $self->dateSeparator();
    if ( '.' eq $sep ) {
        $sep = /\./;
    }
    my $firstPart = ( split( $sep, $self->dateFormat() ) )[0];

    my $result;
    my ( $year, $month, $day );
    if ( 'D' eq $firstPart ) {

        # Expect dates in european format
        #
        if (   ( ( $year, $month, $day ) = Date::Calc::Decode_Date_EU($string) )
            || ( ( $year, $month, $day ) = Date::Calc::Decode_Date_US($string) )
            || ( ( $year, $month, $day ) = _decodeDateAsian($string) ) ) {
            $result = "$year-$month-$day";
        }
    } elsif ( 'M' eq $firstPart ) {

        # Expect dates in US format
        #
        if (   ( ( $year, $month, $day ) = Date::Calc::Decode_Date_US($string) )
            || ( ( $year, $month, $day ) = Date::Calc::Decode_Date_EU($string) )
            || ( ( $year, $month, $day ) = _decodeDateAsian($string) ) ) {
            $result = "$year-$month-$day";
        }
    } elsif ( 'Y' eq $firstPart ) {

        # 'asian' dates.
        #
        if (   ( ( $year, $month, $day ) = _decodeDateAsian($string) )
            || ( ( $year, $month, $day ) = Date::Calc::Decode_Date_US($string) )
            || ( ( $year, $month, $day ) = Date::Calc::Decode_Date_EU($string) ) ) {
            $result = "$year-$month-$day";
        }
    } else {
        die "ERROR - Cannot figure out date format locale from first part $firstPart";
    }

    return $result;
}

sub _decodeDateAsian {
    my ($date) = @_;

    # We're not (yet) going to try and parse date strings that might contain asian month names.
    # I'm hoping by the time we get to that somebody will have added that ability to Date::Calc.
    #
    # We're just going to look for dates in YYYY-MM-DD format, where the separator can be anything.
    # Note that this will sucessfully parse dates in our internal (mysql) format.
    #
    # print STDERR "_decodeDateAsian : in date = $date\n";
    if ( $date =~ /^\d+(.)/ ) {
        my $sep = $1;

        my ( $y, $m, $d ) = split( $sep, $date );
        if (   ( defined $y && $y =~ /^\d+$/ )
            && ( defined $m && $m =~ /^\d+$/ )
            && ( defined $d && $d =~ /^\d+$/ ) ) {

            # Year _might_ be a 2-digit year.
            # If it is, then we want to try and re-interpret it into a current year.
            #
            if ( $y < 100 ) {
                if ( $y >= 50 ) {
                    $y += 1900;
                } else {
                    $y += 2000;
                }
            }

            #print STDERR "_decodeDateAsian : returning $y  $m  $d\n";
            return ( $y, $m, $d );
        }
    }

    return ();
}

#
# --------------------------------------------------------------------------------------------
#

package Common::DateTimeFormat::SettingsReverter;

use lib '/app/tools/common/lib';
use Common::Assert;
use Common::XMLObject;

sub new {
    my ( $class, %args ) = @_;

    my $self = bless {}, $class;

    return $self->_init(%args);
}

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

    my $refToFlags = $args{flags};
    assert( defined $refToFlags, "ERROR - missing flags argument" );

    # If this was C++, this class would be a private memeber class.
    # Hence, it's ok that I'm going to violate encapsulation here...
    #
    $self->{_refToFlags} = $refToFlags;
    foreach my $flag ( keys %{$refToFlags} ) {
        $self->{_oldFlags}{$flag} = $refToFlags->{$flag};
    }

    return $self;
}

sub DESTROY {
    my $self = shift;

    foreach my $flag ( keys %{ $self->{_oldFlags} } ) {
        $self->{_refToFlags}{$flag} = $self->{_oldFlags}{$flag};
    }
}

1;
