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

package RPS::Sale::Report::EMIPulse;
use strict;
use IO::File;
use Date::Calc;
use File::stat;

use lib '/app/tools/common/lib';
use lib '/app/tools/rps/lib';

use RPS::DB::Item::ReportQueries::EMIPulse;
use Common::Client;
use Common::Assert;

use base 'Common::XMLObject';


# JPK - We don't really have a small, well-abstracted base class for any of these asynchronous reports.
# I wish we did, but we don't.
#
# I don't want to spend a ton of time dreaming up the perfect interface.
#
# I think we'll want to have an instance of a 'Sale::Report' object (which is not the current model at all).
# Needs a period_id parameter.
#
# !!! The current model has everything stuffed into a single file, RPS::Sale::Report.
# !!! There is a table that maps a report 'name' (i.e. 'royalty' or 'reconcilliation') to the report code, sort of.
# !!! It's pretty wacky, in my opinion.

# I can't help myself, this code is gross.   So I will start with what I think is a cleaner model for a 'sale report'.
# The current RPS::Report class is structured around web-displayed, paginated reports where the data lives in a table.
# That's not what we're doing here.
# The Report::Dynamic stuff is also totally different.... although perhaps I can structure that?


# !!! These public methods are named the same as the public method in the Report::Dynamic class.
# !!! That is deliberate...
#
# !!! I'd like to transform this into a base class, and refactor the old Report code to use this model.
#


# We may want/need to have this file zipped up.
# So let's keep that in mind...
#

use constant kGSSReportingEntity => 'RS';  # <-- just a placeholder for now.
use constant REPORT_BASE_DIR => '/app/shared/sale_report';
use constant kZIPPath => '/usr/bin/zip';


sub _init
{
    my ($self, %args) = @_;
    assert(defined $args{periodID});
    $self->{periodID} = $args{periodID};

    # The fixed width format is rather challenging to audit.
    # I'll provide an option that allow us to use tab delimiting.
    #
    if ($args{tabDelimited})
    {
        $self->{_useTabs} = 1;   
    }


    # We might want to make the ZIP archiving optional
    #
    if ($args{skipZip})
    {
        $self->{_skipZip} = 1;
    }

    return $self;
}


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


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

    # filename ought to be the name of the file as sent to the user.
    # which may or may not be what we call it on our filesystem.
    #
#    my $fullPath = $self->_internalFullPath();
    my $fullPath = $self->_filePath . '/' . $self->filename();

    return (-e $fullPath);
}


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

    if (! $self->{_filePath})
    {
        $self->{_filePath} = join('/', REPORT_BASE_DIR, Common::Client::Current()->ClientNameClean());
    }

    return $self->{_filePath};
}

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

    return $self->_filePath() . '/' . $self->_internalFilename();
}

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

    # !!! For now, these can be the same.
    #
#    return $self->filename();
   
    # Let's go with something different.
    # We will want to zip up the final file.
    #
    return $self->periodID() . '_Pulse.txt';
}

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

    # Just a guess for now...
    #
    if ($self->{_skipZip})
    {
        return $self->_internalFilename();
    }

    return $self->periodID().'_Pulse.zip';
}

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

    return stat($self->fullPath())->size();
}

sub fullPath
{
    my ($self) = @_;
    my $fullPath = $self->_filePath . '/' . $self->filename();

    return $fullPath;
}

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

    unlink $self->_internalFullPath();
    unlink $self->fullPath();
}


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


    # Might want to just silently fail, but for now let's throw an exception.
    #
    die "ERROR - report file already exists" if $self->exists();


    $self->_create();
}

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

    $self->_openFileForWriting();

    $self->_writeHeader();

    $self->_writeReportData();

    $self->_writeFooter();

    $self->closeFile();
}


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

    my $fullPath = $self->_internalFullPath();


    $self->{_fh} = IO::File->new("> $fullPath")
     or die "ERROR - unable to open $fullPath for writing: $!";

    binmode($self->{_fh}, ':utf8');
}

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

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


    $self->{_fh} = IO::File->new("< $fullPath")
     or die "ERROR - unable to open $fullPath for reading: $!";
}



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

    if ($self->{_fh})
    {
        close $self->{_fh};
        $self->{_fh} = undef;

        # Compress the file.
        #
        if (! $self->{_skipZip} && -e $self->_internalFullPath())
        {
            my $command = kZIPPath . " -j " . $self->fullPath() . ' ' . $self->_internalFullPath();
            my $rVal = system($command);
            if (! $rVal)
            {
                # 0 == success...
                #
                unlink $self->_internalFullPath();
            }
            else
            {
                die "ERROR - Unable to compress " . $self->_internalFullPath() . ": exit code $rVal:  $!";
            }
        }
    }
}

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

    my $time = time;
    my ($year,$month,$day, $hour,$min,$sec, $doy,$dow,$dst) = Date::Calc::Localtime($time);
    my $fileDate = sprintf("%02d%02d%02d", $year, $month, $day);

    
    $self->_writeTextPadded($time, 13);
    $self->_writeTextPadded($fileDate, 6);

    # !!! No clue what the 'GSS Reporting Entity' is yet...
    #
    $self->_writeTextPadded(kGSSReportingEntity, 2);

    $self->_writeTextPadded('HDR', 3);

    # And the fun 371 characters of wasted space...
    #
    $self->_writeTextPadded('', 371);
    $self->_writeTextPadded('0', 1);

    # End the line.
    #
    my $fh = $self->{_fh};
    print $fh "\n";
}


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

    $self->{_unitCount} = 0;

    my $dataCollection = $self->_getCollection();

    while (my $data = $dataCollection->next())
    {
        $self->_writeDataLine($data);
    }
}


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

    $self->_writeTextPadded('', 21);
    $self->_writeTextPadded('FTR', 3);
    $self->_writeTextPadded('', 31);
    $self->_writeIntegerPadded($self->{_unitCount}, 8);
    $self->_writeTextPadded('', 332);
    $self->_writeTextPadded('0', 1);

    my $fh = $self->{_fh};
    print $fh "\n";
}



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

    # I think it's safe to assume that we'll have some query set up to grab the sales we care about.
    # Whether we're going to be joining other tables in this query, well, I just don't know yet.
    # Probably...
    #

    # !!! I don't want to re-use the crappy RPS::File::Sales interface.
    #
    return RPS::DB::Item::ReportQueries::EMIPulse->GetSalesForPeriod($self->periodID());
}

my @gColumns = (
    {
        # Recording Identifier
        method => '_identifier',
        width => 18,
    },
    {
        # Date of Sale
        method => '_columnAsDate',
        field => 'date_end',
        width => 6,
    },
    {
        # Date Invoiced
        width => 6,
    },
    {
        # GSS Reporting Entity
        value => kGSSReportingEntity,
        width => 2,
    },
    {
        # GSS Selling Company
        width => 3,
    },
    {
        # DSP/MSP Code
        width => 8,
    },
    {
        # etailer code
        # !!! Not sure where this will come from...
        width => 8,
    },
    {
        # ReleaseType
        method => '_releaseType',
        width => 3,
    },
    {
        # Single or Multi-track or Other
        width => 1,
    },
    {
        # Quantity
        method => '_columnAsQuantity',
        field => 'units',
        width => 8,
    },
    {
        # Artist
        method => '_artist',
        width => 40,
    },
    {
        # Title
        method => '_title',
        width => 60,
    },
    {
        # ID Type
        method => '_idType',
        width => 1,
    },
    {
        # Segment
        width => 3,
    },
    {
        # currency
        field => 'currency_code',
        width => 3,
    },
    {
        # net value
        method => '_netValue',
        width => 20,
    },
    {
        # Filename
        field => 'filename',
        width => 35,
    },
    {
        # Material Grp
        width => 9,
    },
    {
        # Sales Type
        width => 4,
    },
    {
        # Sales Type Desc
        width => 20,
    },
    {
        # Item Cat
        width => 4,
    },
    {
        # Item Cat Desc
        width => 20,
    },
    {
        # Prod Hierarchy 
        width => 18,
    },
    {
        # Prod Hierarchy Desc
        width => 40,
    },
    {
        # Profit Centre
        width => 10,
    },
    {
        # Profit Centre Desc
        width => 40,
    },
    {
        # Sales Org
        width => 4,
    },
    {
        # Destination Country
        field => 'country_code',  # <- just a guess...
        width => 3,
    },
);

sub _writeDataLine
{
    my ($self, $dbItem) = @_;

    # There are 28 fields.
    # I doubt all of this stuff will end up in the DB::Item directly.
    #
    # The ultimate abstract way would be to create a method for each column.
    # That feels like overkill, since the majority of these columns will just
    # be outputting the value in the DB::Item.
    #
    # I'll use a config hash to make it slightly more abstract.
    #
    foreach my $column (@gColumns)
    {
        my $method = $column->{method};
        $method = '_columnAsText' unless $method;

        $self->$method($column, $dbItem);
    }

    my $fh = $self->{_fh};
    print $fh "\n";
}


sub _columnAsText
{
    my ($self, $column, $dbItem) = @_;
    assert($column->{width});

    my $value;
    if (defined $column->{value})
    {
        $value = $column->{value};
    }
    elsif ($column->{field})
    {
        my $f = $column->{field};
        $value = $dbItem->$f();
    }

    $self->_writeTextPadded($value, $column->{width});
}

sub _columnAsDate
{
    my ($self, $column, $dbItem) = @_;
    assert($column->{width});
    assert($column->{field});

    my $f = $column->{field};
    my $value = $dbItem->$f();

    my @chunks = split('-', $value);
    my $y = substr($chunks[0], 2, 2);
    my $m = $chunks[1];
    my $d = $chunks[2];

    $self->_writeTextPadded(sprintf("%02d%02d%02d", $y, $m, $d), $column->{width});
}


sub _columnAsQuantity
{
    my ($self, $column, $dbItem) = @_;
    assert($column->{width});
    assert($column->{field});

    my $f = $column->{field};
    my $value = $dbItem->$f();

    $self->{_unitCount} += $value;

    $self->_writeIntegerPadded($value, $column->{width});
}


sub _netValue
{
    my ($self, $column, $dbItem) = @_;
    assert($column->{width});

    my $units = $dbItem->units();
    my $price = $dbItem->price();

    # Anything else I need to do here?  Currency conversion, etc?
    #
    $self->_writeDecimalPadded(($units * $price), $column->{width});
}


sub _identifier
{
    my ($self, $column, $dbItem) = @_;
    assert($column->{width});

    # Which column we look at will depend on the product type.
    #
    my $id;
    if ($dbItem->product_type eq 'T')
    {
        $id = $dbItem->digital_isrc;
    }
    else
    {
        $id = $dbItem->digital_icpn;
    }

    $self->_writeTextPadded($id, $column->{width});
}

sub _releaseType
{
    my ($self, $column, $dbItem) = @_;
    assert($column->{width});

    # Which column we look at will depend on the product type.
    #
    my $type;
    if ($dbItem->product_type eq 'T')
    {
        $type = 'S';
    }
    else
    {
        $type = 'M';
    }

    $self->_writeTextPadded($type, $column->{width});
}

sub _idType
{
    my ($self, $column, $dbItem) = @_;
    assert($column->{width});

    # Which column we look at will depend on the product type.
    #
    my $type;
    if ($dbItem->product_type eq 'T')
    {
        $type = 'I';
    }
    else
    {
        $type = 'U';
    }

    $self->_writeTextPadded($type, $column->{width});
}

sub _artist
{
    my ($self, $column, $dbItem) = @_;
    assert($column->{width});

    # Which column we look at will depend on the product type.
    #
    my $name;
    if ($dbItem->product_type eq 'T')
    {
        $name = $dbItem->track_artist;
    }
    else
    {
        $name = $dbItem->album_artist;
    }

    $self->_writeTextPadded($name, $column->{width});
}

sub _title
{
    my ($self, $column, $dbItem) = @_;
    assert($column->{width});

    # Which column we look at will depend on the product type.
    #
    my $name;
    if ($dbItem->product_type eq 'T')
    {
        $name = $dbItem->track_title;
    }
    else
    {
        $name = $dbItem->album_title;
    }

    $self->_writeTextPadded($name, $column->{width});
}

sub _writeTextPadded
{
    my ($self, $string, $width) = @_;

    my $fh = $self->{_fh};

    # Truncate the string if necessary
    #
    $string = substr($string, 0, $width);

    my $padding = $width - length($string);
    if (length($string))
    {
        print $fh $string;
    }

    if ($self->{_useTabs})
    {
        print $fh "\t";
    }
    else
    {
        if ($padding)
        {
            print $fh ' ' x $padding;
        }
    }
}

sub _writeIntegerPadded
{
    my ($self, $number, $width) = @_;
    my $paddedString = sprintf("%0".$width."d", $number);

    my $fh = $self->{_fh};
    print $fh $paddedString;

    if ($self->{_useTabs})
    {
        print $fh "\t";
    }
}

sub _writeDecimalPadded
{
    my ($self, $number, $width) = @_;
    assert($width > 6);

    # Going to assume that we reserve 6 characters for the denominator.
    # All the rest are used for the minus sign, if any, the numerator, and the decimal point.
    #
    # !!! Note, not using RSMath rounding here....
    #
    my $paddedString = sprintf("%0".$width.".06f", $number);

    my $fh = $self->{_fh};
    print $fh $paddedString;

    if ($self->{_useTabs})
    {
        print $fh "\t";
    }
}



1;
