package BookPub::Catalog::Import::Parser::ONIX;

use strict;

use XML::Simple;
use XML::Entities;

use Data::Dumper;

use lib '/app/tools/common/lib';
use Common::UTF8;
use lib '/app/tools/bookpub/lib';
use base 'BookPub::Catalog::Import::Parser';

my %ALT_TAG_NAMES = (
    m182 => 'sentdate',

    # record id
    a001 => 'recordreference',
    a002 => 'notificationtype',
    a198 => 'deletioncode',
    a199 => 'deletiontext',

    # product id
    b211 => 'epubtype',
    b221 => 'productidtype',
    b244 => 'idvalue',
    b012 => 'productform',
    b333 => 'productformdetail',

    # title
    b202 => 'titletype',    # will need alt_title table if we see anything other than 01
    b203 => 'titletext',

    # contributor
    b034 => 'sequencenumber',
    b035 => 'contributorrole',
    b036 => 'personname',
    b037 => 'personnameinverted',    # Last, First
    b039 => 'namesbeforekey',        # first name
    b040 => 'keynames',              # last name
    b047 => 'corporatename',

    # edition
    b057 => 'editionnumber',
    b217 => 'editionversionnumber',

    # Number within series (closest thing to 'Volume' I could find)
    b019 => 'numberwithinseries',

    # Subtitle
    b029 => 'subtitle',

    # language
    b252 => 'languagecode',
    b253 => 'languagerole',

    # imprint / publisher
    b241 => 'typecode',           # should all be 02 - proprietary
    b243 => 'value',
    b291 => 'pubrole',            # applies to pub only - so far all 01
    b079 => 'imprintname',
    b081 => 'publishername',
    b394 => 'publishingstatus',

    # pub dates
    b003 => 'publicationdate',
    h134 => 'outofprintdate',

    # pub location
    b083 => 'countryofpublication',

    # sales rights
    b089 => 'typecode',        # 01 = exclusive, 02 = non-exclusive, 03 = exclude(country list)
    b090 => 'countrycode',
    b388 => 'territorycode',

    # others
    b061               => 'numberofpages',
    b064               => 'basicmainsubject',    # BISAC is the right acronym, but the onix spec used BASIC, and it cannot now be changed.
    'bisacmainsubject' => 'basicmainsubject',
    b073               => 'audiencecode',
    f117               => 'imagefile',
    h208               => 'relationcode',

    # pricing info
    j148 => 'pricetypecode',
    j151 => 'priceamount',
    j152 => 'currencycode',
    b251 => 'countrycode',
    j261 => 'pricequalifier',
    j263 => 'minorderqty',
    j363 => 'discountcodetype',
    j364 => 'discountcode',
    j378 => 'discountcodetypename',
    j396 => 'productavailability',
    j138 => 'supplytocountry',
    j142 => 'expectedshipdate',
    j292 => 'supplierrole',
    j141 => 'availabilitycode',
    j137 => 'suppliername',
    j161 => 'priceeffectivefrom',

    # image file
    f114 => 'mediafiletypecode',        # list 38
    f115 => 'mediafileformatcode',      # list 39
    f116 => 'mediafilelinktypecode',    # list 40
    f117 => 'mediafilelink',

    # misc
    a196 => 'recordsourcename',
    b067 => 'subjectschemeidentifier',
    b069 => 'subjectcode',
);

# various book-type mappings
my %RightsMap = (
    '01' => 'exclusive',
    '02' => 'non-exclusive',
    '03' => 'exclude',
);

my %ProductIDMap = (
    '01' => 'Proprietary',
    '02' => 'ISBN-10',
    '03' => 'EAN',
    '04' => 'UPC',

    #05      => 'ISMN',
    #06      => 'DOI',
    #13      => 'LCCN',
    14 => 'GTIN',
    15 => 'ISBN-13',

    #17      => 'Legal Deposit Number',
    #22      => 'URN',
);

sub header {
    return $_[0]->{header};
}

sub productFileVersion {
    return $_[0]->{_product_file_version};
}

sub nextProduct {
    my $self = shift;
    my $xml  = '';
    my $skip = 0;

    my $FH = $self->{FH};

    # catch a <product>..</product> block
    while (<$FH>) {

        # RSD-7902 modify string if there is no newline
        $_ =~ s/<product><\/product>/<product>/i;
        $_ =~ s/<\/product><product>/<\/product>/i;

        next unless ( $xml || $_ =~ /<product>/i );

        # certain tags tend to contain special chars that screw up the XML parser
        # we don't really care about the contents (currently), so ignore them
        # !!! Make sure we ignore preceeding or trailing white space...
        # !!! Need to leave off the closing '>', in case there are XML attributes in these tags...
        # skip if the pattern exists
        if ( $_ =~ /^\s*<(d104|b044)/ ) {
            $skip = 1;
        }
        if ( $_ =~ /<\/(d104|b044)>/ ) {
            $skip = 0;
            next;
        }
        next if $skip;

        # We need to get rid of everything before the opening product tag
        if ( $_ =~ /^.*(<product>.*)/i ) {
            $_ = $1;
        }

        $xml .= $_;

        last if $_ =~ /^.*<\/product>/i;
    }

    return unless $xml;

    # !!! Convert tag names to lower case !!!
    # !!! But don't mess with <! > tags...
    $xml =~ s/<([^!])([^>]*)>/'<'.lc($1).lc($2).'>'/eg;

    # strip the potential <text> tag as it sometimes causes issues
    $xml =~ s/<othertext>.*?<\/othertext>//igs;

    # replacing codes by text values
    while ( my ( $k, $v ) = each %ALT_TAG_NAMES ) {
        eval { $xml =~ s/<(\/)?$k([a-z0-9="'\s]+)?>/<$1$v>/ig; };
        if ($@) {
            print "\nbad xml: $xml\n";
            exit();
        }
    }

    # Force conversion to utf8.
    $xml = Common::UTF8::Encode($xml);

    # Replace wacky characters (like the vertical tab - ascii 0x0b) with spaces.
    $xml =~ s/([\000-\010]|[\012-\037])/ /g;

    # convert any alpha entities to decimal
    XML::Entities::numify( 'all', $xml );

    # !!! Exciting new hack!
    # So, it turns out that XMLin, if passed a string, can't actually handle utf8 characters.
    # And there is no way remaining to inform the parser to expect utf8.
    # However... if we're reading in a file, and that file does have the proper 'utf8' header,
    # then apparently it will work just fine.
    #
    # Therefore, in order to not have to strip out UTF8 characters or perform other nonsense,
    # we'll write out the current $xml contents (which should consist of a single <Product> block)
    # to a temporary file, and pass THAT to XMLin.

    my $tempFileName = '/tmp/' . $$ . time() . '.xml';
    open TEMPFILE, ">$tempFileName" or die "ERROR - unable to open $tempFileName for writing: $!";
    binmode( TEMPFILE, ":utf8" );
    print TEMPFILE '<?xml version="1.0" encoding="utf-8"?>' . "\n";
    print TEMPFILE "$xml\n";
    close TEMPFILE;

    # transform XML to a HASH ref
    my $prodRef = XMLin( $tempFileName, NoAttr => 1, SuppressEmpty => undef );

    # now format and translate some values
    $prodRef->{outofprintdate}  =~ s/^(\d{4})(\d{2})(\d{2})/$1-$2-$3/;
    if ( $self->productFileVersion == 3 ) {
        $prodRef->{publicationdate} = ref $prodRef->{publishingdetail}{publishingdate} eq 'ARRAY'
            ? $prodRef->{publishingdetail}{publishingdate}[0]{date}
            : $prodRef->{publishingdetail}{publishingdate};
    }
    $prodRef->{publicationdate} =~ s/^(\d{4})(\d{2})(\d{2})/$1-$2-$3/;

    my $productIDs = $prodRef->{productidentifier};
    $productIDs = [ $productIDs ] unless 'ARRAY' eq ref($productIDs);

    foreach my $prodMap (@$productIDs) {
        unless ( $ProductIDMap{ $prodMap->{productidtype} } ) {
            print STDERR "no map for $prodMap->{productidtype}\n";
            next;
        }
        $prodMap->{idtype} = $ProductIDMap{ $prodMap->{productidtype} };
    }

    if ( ref( $prodRef->{salesrights} ) eq 'ARRAY' ) {
        foreach my $rights ( @{ $prodRef->{salesrights} } ) {
            # TODO: it seems that v2.1 does not contain the fields below, possibly the previous version did
            # a simple workaround to fix this
            $rights->{typecode}      //= $rights->{salesrightstype};
            $rights->{countrycode}   //= $rights->{rightscountry};
            $rights->{territorycode} //= $rights->{rightsterritory};

            $rights->{typecode} = $RightsMap{ $rights->{typecode} };
            $rights->{countrycode} =~ s/\W+/,/g;
            $rights->{territorycode} =~ s/\W+/,/g if ( length $rights->{territorycode} );
        }
    }

    # unused - for now
    delete $prodRef->{measure};

    # Stash a copy of this product's XML in the hash ref - This will make it a tad
    # easier to provide some context if there is an error downstream.

    $prodRef->{_xml} = $xml;

    unlink $tempFileName;

    return $prodRef;
}

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

    die "$args{file} non-existent or empty\n" unless ( -s $args{file} );
    open( $FH, '<', $args{file} ) or die "can't open file - $!\n";

    my $header = '';
    my $xmlTag = <$FH>;

    while ( my $row = <$FH> ) {
        $row =~ s/<([^!])([^>]*)>/'<'.lc($1).lc($2).'>'/eg;
        next unless ( $header || $row =~ /^\s*<header>/i );

        # We need to get rid of everything after the closing header tag.
        if ( $row =~ /^(\s*<\/header>)/i ) {
            $row = $1;
        }
        $header .= $row;
        last if ( $row =~ /^\s*<\/header>/i );
    }

    $self->{header} = XMLin( $header, NoAttr => 1 );

    # Reset the file handle first.
    seek $FH, 0, 0;

    $self->{FH} = $FH;

    # we must do this only once for the particular version of the format
    $self->_determineProductFileVersion();
    $self->_modifyAltTagCodesToV3();

    return 1;
}

sub _determineProductFileVersion {
    my $self = shift;

    $self->{_product_file_version} = 0;
    if ( exists $self->{header}{sentdate} || exists $self->{header}{m182} ) {
        $self->{_product_file_version} = 2;
    } elsif ( exists $self->{header}{sentdatetime} || exists $self->{header}{x307} ) {
        $self->{_product_file_version} = 3;
    }

    return 1;
}

sub _modifyAltTagCodesToV3 {
    my $self = shift;

    return unless $self->productFileVersion == 3;

    # here we will change v2.1 codes to v3.x with new tag names

    # sentdate => (Header => SentDateTime)
    $ALT_TAG_NAMES{x307} = 'sentdatetime'; delete $ALT_TAG_NAMES{m182};

    # TODO: need to double-check
    # epubtype => EpubUsageType
    # $ALT_TAG_NAMES{x318} = 'epubusagetype'; delete $ALT_TAG_NAMES{b211};

    # typecode => (Product => DescriptiveDetail => Measure => MeasureType)
    $ALT_TAG_NAMES{x315} = 'measuretype'; delete $ALT_TAG_NAMES{b241};

    # relationcode => (Product => RelatedMaterial => RelatedProduct => ProductRelationCode)
    $ALT_TAG_NAMES{x455} = 'productrelationcode'; delete $ALT_TAG_NAMES{h208};

    # pricetypecode => (Product => ProductSupply => SupplyDetail => Price => PriceType)
    $ALT_TAG_NAMES{x462} = 'pricetype'; delete $ALT_TAG_NAMES{j148};

    # recordsourcename => (Product => RecordSourceName)
    $ALT_TAG_NAMES{a197} = 'recordsourcename'; delete $ALT_TAG_NAMES{a196};

    # publicationdate => (Product => PublishingDetail => PublishingDate => {Date, PriceDateRole})
    $ALT_TAG_NAMES{b306} = 'date'; delete $ALT_TAG_NAMES{b003};
    $ALT_TAG_NAMES{x476} = 'pricedaterole';

    # audiencecode -> (Product => DescriptiveDetail => Audience => AudienceCodeValue)
    $ALT_TAG_NAMES{b206} = 'audiencecodevalue'; delete $ALT_TAG_NAMES{b073};

    # (Product => ProductSupply => Market => Territory => CountriesIncluded)
    $ALT_TAG_NAMES{x449} = 'countriesincluded';

    $ALT_TAG_NAMES{x409} = 'titletype';
    $ALT_TAG_NAMES{b030} = 'titleprefix';
    $ALT_TAG_NAMES{b031} = 'titlewithoutprefix';

    return 1;
}


1;
