#---------------------------------------------------------------
# ____                   _ _         ____  _
#|  _ \ ___  _   _  __ _| | |_ _   _/ ___|| |__   __ _ _ __ ___
#| |_) / _ \| | | |/ _` | | __| | | \___ \| '_ \ / _` | '__/ _ \
#|  _ < (_) | |_| | (_| | | |_| |_| |___) | | | | (_| | | |  __/
#|_| \_\___/ \__, |\__,_|_|\__|\__, |____/|_| |_|\__,_|_|  \___|
#            |___/             |___/
#
# Copyright (C) 2011 RoyaltyShare, Inc.   All Rights Reserved
# $Id$
#---------------------------------------------------------------

package BookPub::Analytics::ProductMonitor::Data;
use strict;

use lib '/app/tools/common/lib';
use lib '/app/tools/bookpub/lib';
use Common::XMLObject;
use Common::Assert;
use BookPub::DB::Item::BookProduct;
use BookPub::DB::Item::ProductMarketPrice;

# !!! NOT going to make this an XML or Form object.
# !!! This class just intends to capture and organize a set of data series.
#
sub new {
    my ( $class, %args ) = @_;

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

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

    # dateStart and dateEnd are required.
    #
    assert( $args{dateStart} );
    assert( $args{dateEnd} );

    $self->{dateStart} = $args{dateStart};
    $self->{dateEnd}   = $args{dateEnd};

    # product id is required, as is service id.
    # serviceIDs should be an array reference of ids.
    # !!! So, rather than use a distinct object for each service, we're going
    # to lump them all together.  This is so that we will only need to iterate
    # over the range of dates once for everything.
    #
    assert( $args{productID} );
    assert( $args{serviceID} );

    $self->{productIDs} = 'ARRAY' eq ref( $args{productID} ) ? $args{productID} : [ $args{productID} ];
    $self->{serviceIDs} = 'ARRAY' eq ref( $args{serviceID} ) ? $args{serviceID} : [ $args{serviceID} ];

    # !!! It would be wise to sanity-check these dates. Make sure they are dates,
    # that dateEnd > dateStart, etc.

    # We'll load the data lazily.
    #

    return $self;
}

sub serviceIDs {
    my ($self) = @_;

    return $self->{serviceIDs};
}

sub getServiceDataForProductID {
    my ( $self, $serviceID, $productID ) = @_;
    assert($serviceID);
    assert($productID);

    if ( !$self->{serviceData} ) {
        $self->_loadData();
    }

    return $self->{serviceData}{$serviceID}{$productID};
}

sub getDates {
    my ($self) = @_;

    if ( !$self->{dates} ) {
        $self->_loadData();
    }
    return $self->{dates};
}

sub _getSimilarProducts {
    my ( $self, $productID ) = @_;
    assert($productID);

    # By default we'll use the most restrictive notion of similarity.
    # For eBooks, that means products that share the same expected price.
    # Now, price exists at a moment in time... I'll try the end date of our date range.
    #
    my $productMarket = $self->_productMarket($productID);
    my $marketID      = $productMarket->market_id();
    my $currentPrice  = BookPub::DB::Item::ProductMarketPrice->GetCurrentPrice(
        product_market_id => $productMarket->product_market_id(),
        date              => $self->{dateEnd},
        currency_code     => $self->_currencyCode(),
    );

    my $price = $currentPrice->price();

    my @productIDArray = BookPub::DB::Item::BookProduct->GetProductIDsOfSimilarProducts(
        $productID,
        marketID     => $marketID,
        currencyCode => $self->_currencyCode(),
        price        => $price,
    );

    return \@productIDArray;
}

sub _loadData {
    my ($self) = @_;

    my $serviceIDs = $self->serviceIDs();

    foreach my $productID ( @{ $self->{productIDs} } ) {
        $self->_getDataCollections( $serviceIDs, $productID );
    }

    # Now we start building the 'flattened' time series.
    # !!! I'll need a mechanism, possibly involving Date::Calc, to iterate over all the days from the first day to the last.
    # !!! Use Date::Calc::Add_Delta_Days to increment the y/m/d count.
    #
    my ( $startYear, $startMonth, $startDay ) = split( /-/, $self->{dateStart} );
    my ( $endYear,   $endMonth,   $endDay )   = split( /-/, $self->{dateEnd} );

    # We're going to add one more day to the end date, so we will run prices _through_ the original end date.
    #
    ( $endYear, $endMonth, $endDay ) = Date::Calc::Add_Delta_Days( $endYear, $endMonth, $endDay, 1 );

    my $y = $startYear;
    my $m = $startMonth;
    my $d = $startDay;

    do {
        # Record each date, so we can return that for convenience sake...
        #
        push @{ $self->{dates} }, Common::Client::Current()->Locale()->formatDate( sprintf( "%04d-%02d-%02d", $y, $m, $d ) );

        foreach my $productID ( @{ $self->{productIDs} } ) {
            $self->_handleDate( $productID, $y, $m, $d, $serviceIDs );
        }

        # Increment the date
        #
        ( $y, $m, $d ) = Date::Calc::Add_Delta_Days( $y, $m, $d, 1 );

    } until ( $y >= $endYear && $m >= $endMonth && $d >= $endDay );

}

# All of these data series classes will be loading info for one or more services, so we can have a generic structure to
# drive that.  Some classes (like the monitored prices) may need to override this to fetch some additional stuff.
#
sub _getDataCollections {
    my ( $self, $serviceIDs, $productID ) = @_;

    # Fetch all the 'similar' product ids.
    #
    my $productIDs = $self->_getSimilarProducts($productID);

    # Fetch the data collections for each service.
    #
    foreach my $serviceID (@$serviceIDs) {
        my $collection = $self->_getServiceDataCollection( $serviceID, $productIDs );

        $self->{_serviceDataCollections}{$serviceID}{$productID} = $collection;
    }

    # Prime the service iterator mechanism.
    #
    foreach my $serviceID (@$serviceIDs) {
        $self->{_currentServiceData}{$serviceID}{$productID} = undef;
        $self->{_nextServiceData}{$serviceID}{$productID}    = $self->{_serviceDataCollections}{$serviceID}{$productID}->next();
    }
}

sub _handleDate {
    my ( $self, $productID, $y, $m, $d, $serviceIDs ) = @_;

    foreach my $serviceID (@$serviceIDs) {

        # !!! Let's get this algorithm straight.
        # If the current price is undefined:
        # - keep iterating over the data items until we find a 'next item' that's in the future, or we
        #   run out of items.
        # If we have a current item:
        # - keep current and next the same unless next is defined, and in the past.  Then make next the current item.
        #
        if ( !defined $self->{_currentServiceData}{$serviceID}{$productID} ) {
            while ( defined $self->{_nextServiceData}{$serviceID}{$productID} ) {
                my ( $priceYear, $priceMonth, $priceDay ) =
                  Common::Util::decodeDateMysql( $self->_dateFromItem( $self->{_nextServiceData}{$serviceID}{$productID} ) );
                my $delta = Date::Calc::Delta_Days( $y, $m, $d, $priceYear, $priceMonth, $priceDay );

                if ( $delta > 0 ) {

                    # The next item is in the future.  So whatever current item we have (or don't have) is now the true current item.
                    #
                    last;
                }

                # We're here because the next item is still in the 'past' (relative to the date we're looking at),
                # so keep iterating.
                #
                $self->{_currentServiceData}{$serviceID}{$productID} = $self->{_nextServiceData}{$serviceID}{$productID};
                $self->{_nextServiceData}{$serviceID}{$productID}    = $self->{_serviceDataCollections}{$serviceID}{$productID}->next();
            }
        } else {

            # We have a current item already.   Keep that unless the next item is no longer in the future.
            #
            if ( defined $self->{_nextServiceData}{$serviceID}{$productID} ) {
                my ( $priceYear, $priceMonth, $priceDay ) =
                  Common::Util::decodeDateMysql( $self->_dateFromItem( $self->{_nextServiceData}{$serviceID}{$productID} ) );
                my $delta = Date::Calc::Delta_Days( $y, $m, $d, $priceYear, $priceMonth, $priceDay );

                # !!! In theory, the '<' is not necessary here...
                #
                if ( $delta <= 0 ) {

                    # The next item is in the past, so it becomes the _potential_ current item.
                    #
                    $self->{_currentServiceData}{$serviceID}{$productID} = $self->{_nextServiceData}{$serviceID}{$productID};
                    $self->{_nextServiceData}{$serviceID}{$productID}    = $self->{_serviceDataCollections}{$serviceID}{$productID}->next();
                }
            }
        }

        # Now we push this data onto the data array.
        #
        my $data;
        if ( defined $self->{_currentServiceData}{$serviceID}{$productID} ) {
            $data = $self->_dataElementFromItem( $self->{_currentServiceData}{$serviceID}{$productID} );
        }

        push @{ $self->{serviceData}{$serviceID}{$productID} }, $data;

        # By default we keep the current item around until we find a new current item.
        # But this doesn't make sense for all of our data (like revenue).
        # So I'm going to stick a little method in here we can override to change that behavior.
        #
        if ( !$self->_currentDataCarriesOver() ) {
            $self->{_currentServiceData}{$serviceID}{$productID} = undef;
        }
    }
}

# Abstracting this out so we can mess with it easily.
#
sub _countryCode {
    my ($self) = @_;

    # !!! At the moment we hard-code this.  Perhaps some day this will become a parameter.
    #
    return 'US';
}

sub _currencyCode {
    my ($self) = @_;
    return 'USD';
}

# This will return the DB::Item::ProductMarket associated with the product_id argument and the
# desired country code.
#
sub _productMarket {
    my ( $self, $productID ) = @_;
    assert($productID);

    if ( !$self->{_productMarket}{$productID} ) {
        my $usRegions = BookPub::DB::Item::Region->GetAllRegions( country_code => $self->_countryCode() );
        die "ERROR - unable to find a region for " . $self->_countryCode() unless $usRegions->size() > 0;

        my @regionIDs;
        while ( my $region = $usRegions->next() ) {
            push @regionIDs, $region->region_id;
        }

        my $productMarkets = BookPub::DB::Item::ProductMarket->GetAllByRegion( region_id => \@regionIDs, product_id => $productID );

        # !!! I am going to assume just 1.   This might be false...
        #
        die "ERROR - More than one product_market associated with product: $productID, country " . $self->_countryCode()
          if ( $productMarkets->size() > 1 );

        die "ERROR - No product_market found for product: $productID, country " . $self->_countryCode()
          if ( $productMarkets->size() == 0 );

        my $productMarket = $productMarkets->next();
        $self->{_productMarket}{$productID} = $productMarket;
    }

    return $self->{_productMarket}{$productID};
}

sub _currentDataCarriesOver {
    my ($self) = @_;

    # Data carries over by default
    #
    return 1;
}

# Override this method to return a DB::ItemCollection
#
sub _getServiceDataCollection {
    my ( $self, $serviceID, $productIDs ) = @_;
    assert( 0, 'override' );
}

# Override this method to return the 'data payload' from a DB::Item.
# The DB::Item passed in will be whatever is contained in the collection
# returned by _getServiceDataCollection().
# This will be the data that gets stuffed into the data series arrays (price, ranking, units, whatever).
#
# !!! This had been called 'dataFromItem', but that was a bit too similar to 'dateFromItem'...
#
sub _dataElementFromItem {
    my ( $self, $item ) = @_;
    assert( 0, 'override' );
}

# Override this method to return the date value (in mysql format) to use when sorting and organizing
# the data series.
# The DB::Item passed in will be whatever is contained in the collection
# returned by _getServiceDataCollection().
#
sub _dateFromItem {
    my ( $self, $item ) = @_;
    assert( 0, 'override' );
}

###
1;    # Play nicely;
###
