#------------------------------------------------------------
# Copyright (C) 2006 RoyaltyShare, Inc.   All Rights Reserved
#------------------------------------------------------------
package TableObj;
use strict;

use Data::Dumper;
use PDF::API2;

use lib '/app/tools/rps/bin/statements';
use EmbedImage;

use constant kDefaultColPadding    => 2;
use constant kDefaultRowPadding    => 2;
use constant kDefaultPenSize       => 1;
use constant kDefaultTopLinePad    => 1;
use constant kDefaultBottomLinePad => 1;
use constant kDefaultLineColor     => 'black';

#
# my $table = TableObj->new
# (
#   $pdf;           # A reference to a PDF::API2 object, which will provide the interface for PDF functions.
#   $page;          # A reference to a PDF::API2::Page object, on which we'll draw the Table.
#                   # !!! Is this strictly necessary?  I would prefer to delay providing the actual page
#                   # !!! until the 'print' method.
#   $dataArrayRef;  # A reference to a 2-dimentional array of scalars, which form the table's content.
#   !!! Really, why bother with 3 args that are not in the hash, if some of the hash args are _required_?
#
#   bottom_margin   => $anInteger,          # The bottom margin (in PostScript points) of each page.
#   new_page_y      => $anInteger,          # The Y coordinate (in PostScript points) at which to continue
#                                           # drawing when a new page is created.
#   new_page_func => \&aFunctionRef,        # _optional_ - a reference to a function of the following form:
#                                           # my $page = func($pdf);  - pdf is a PDF::API2 object, $page is a
#                                                                     - PDF::API2::Page object
#   font            => $aFontRef,           # The default font.
#   font_size       => $anInteger,          # The default font size.
#
#   # The following args are _optional_
#   #
#   column_props =>                         # This arg allows you to specify properties for each column.
#   [                                       # It takes a reference to an array of hash refs, one hash ref per column.
#                                           # All these properties are optional.
#       {
#           font        => $aFontRef,       # The font to use for this column.
#           font_size   => $anInteger,      # The font size to use for this column.
#           justify     => $aString,        # The column justification. Can be 'left' (default), 'right', or 'center'
#           min_w       => $anInteger,      # The minimum width (in PostScript points).
#           pad         => $anInteger,      # How much padding (in PostScript points) between this column and the column
#                                           # to the left.  This is ignored for the left-most column.
#       },
#       # ... Remember, need a hash ref (even an empty one) for each column if you are going
#       #     to use this argument.
#   ],
#   row_props =>                            # This arg allows you to specify properties for each row.
#                                           # It takes a reference to an array of hash refs, one hash ref per row.
#   [                                       # All these properties are optional.
#       {
#           font        => $aFontRef,       # The font to use for this row.
#           font_size   => $anInteger,      # The font size to use for this row.
#           pad         => $anInteger,      # How much padding (in PostScript points) between this row and the row above it.
#                                           # This is ignored for the top-most row.
#
#           lines =>                        # A reference to an array of hash refs that define a set of horizontal lines to
#                                           # draw in this row.
#                                           # Lines will be drawn directly beneath any text in the row.
#                                           # You can have as many 'line definitions' as you need.
#           [
#               {                           # Each line definition has the following properties:
#                   start_col   => $i1,         # The index (0-based) of the column in which to start drawing the line.
#                                               #   The line will begin in the left-most corner of the cell (not including any pad).
#                   end_col     => $i2,         # The index (0-based) of the column in which to stop drawing the line.
#                                               #   The line wil end in the right-most corner of that column, and will span any padding
#                                               #   between columns (in other words, it will be a continuous line from $i1 to $i2)
#                   top_pad     => $anInteger,  # _optional_  The amount of padding (in PostScript points) between the
#                                               #   top of the column OR the bottom of the text (if any), and the line.
#                   bottom_pad  => $anInteger,  # _optional_  The amount of padding between the bottom of the line and
#                                               #   the bottom of the row.
#                   pen_size    => $anInteger,  # _optional_  The height (in PostScript points) of the line. Default is 1.
#               },
#               # ... You can define as many lines in a row as you need
#           ],
#       },
#       # ... Remember, you will need a hash ref (even an empty one) for each row if you are
#       #     going to use this argument.
#   ],
# );
#
# # !!! Note that in general row_props override column_props.
# # !!! We might need to make that more consistent.
# # !!! We might want to provide a way to change that (i.e. make column settings override row settings if necessary).
#
# # TODO - Might want to provide a way to declare special options for the 'header' row (for example, to have it print
# #        on each page).
#
#
sub new {
    my ( $class, $pdf, $startPage, $dataArray, %args ) = @_;

    my $self = bless {}, $class;

    return $self->_init( $pdf, $startPage, $dataArray, %args );
}

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

    $self->{pdf}  = $pdf;
    $self->{page} = $startPage;
    $self->{data} = $dataArray;
    $self->{text} = $self->{page}->text();
    $self->{gfx}  = $self->{page}->gfx();

    $self->{font}     = $args{font};
    $self->{fontSize} = $args{font_size};

    $self->{numRows}    = scalar(@$dataArray);
    $self->{numColumns} = scalar( @{ $dataArray->[0] } );

    $self->{newPageFunc} = $args{new_page_func};

    my $linkArray = $args{row_links};

    # The bottom margin.
    #
    $self->{bottomMargin} = $args{bottom_margin};

    # Where to start the table when we start a new page.
    #
    $self->{newPageY} = $args{new_page_y};

    # Look for optional args
    #
    #    $self->{rowPadding} = defined $args{row_padding} ? $args{row_padding} : kDefaultRowPadding;
    #    $self->{columnPadding} = defined $args{column_padding} ? $args{column_padding} : kDefaultColPadding;

    # Build our own 2-d array, which will contain the text and font info for every cell.
    # As we go, we'll keep updating the cellWidth and rowHeight data.
    #
    my @collWidth;
    my @rowHeight;
    my @rowLines;

    my @cells;
    for ( my $j = 0 ; $j < $self->{numRows} ; $j++ ) {
        my @row;
        my %lineData;

        if ( 0 == $j ) {
            $self->{rowPadding}[$j] = 0;
        } else {
            $self->{rowPadding}[$j] = defined $args{row_padding} ? $args{row_padding} : kDefaultRowPadding;

            # Any specific row properties we should take into account?
            #
            if ( defined $args{row_props} ) {
                if ( defined $args{row_props}[$j]{pad} ) {
                    $self->{rowPadding}[$j] = $args{row_props}[$j]{pad};
                }
            }
        }

        # See if the 'repeat' row is being declared.
        #
        if ( defined $args{row_props} && $args{row_props}[$j]{repeat} ) {
            $self->{repeatRow} = $j;
        }

        # Now see if the 'repeatRows' flag is set.  This allows multiple rows to be repeated.
        #
        elsif ( defined $args{row_props} && $args{row_props}[$j]{repeatRows} ) {
            $self->{repeatRows} .= "-" . $j;
        }

        for ( my $i = 0 ; $i < $self->{numColumns} ; $i++ ) {
            my $width    = 0;
            my $maxWidth = 0;
            my %cell;
            $cell{text} = $dataArray->[$j][$i];
            $cell{link} = $linkArray->[$j][$i];

            $cell{font}          = $self->{font};
            $cell{fontSize}      = $self->{fontSize};
            $cell{justify}       = 'left';
            $cell{image}         = '';
            $cell{doNotTruncate} = '';
            $cell{pad}           = kDefaultColPadding;

            my $columnPadding = defined $args{column_padding} ? $args{column_padding} : kDefaultColPadding;

            # If there are column_props defined, use those to override default values.
            #
            if ( defined $args{column_props} ) {
                my $default = $args{column_props}->[$i];

                $cell{font}          = $default->{font}            if ( defined $default->{font} );
                $cell{fontSize}      = $default->{font_size}       if ( defined $default->{font_size} );
                $cell{justify}       = $default->{justify}         if ( defined $default->{justify} );
                $cell{image}         = $default->{image}           if ( defined $default->{image} );
                $cell{doNotTruncate} = $default->{do_not_truncate} if ( defined $default->{do_not_truncate} );
                $columnPadding       = $default->{pad}             if ( defined $default->{pad} );

                $width    = $default->{min_w} if ( defined $default->{min_w} );
                $maxWidth = $default->{max_w} if ( defined $default->{max_w} );
            }

            # We don't add padding to the left-most column.  That would be goofy.
            #
            if ( 0 == $i ) {
                $self->{columnPadding}[$i] = 0;
            } else {
                if ( !defined $self->{columnPadding}[$i] || $columnPadding > $self->{columnPadding}[$i] ) {
                    $self->{columnPadding}[$i] = $columnPadding;
                }
            }

            # If this is the header line, look for header overrides.
            # !!! I may change this a bit...
            #
            if ( 0 == $j && defined $args{header_props} ) {
                $cell{font}     = $args{header_props}{font}      if ( defined $args{header_props}{font} );
                $cell{fontSize} = $args{header_props}{font_size} if ( defined $args{header_props}{font_size} );
            }

            # Look for row-level property overrides
            #
            if ( defined $args{row_props} ) {
                $cell{font}     = $args{row_props}->[$j]->{font}      if ( defined $args{row_props}->[$j]->{font} );
                $cell{fontSize} = $args{row_props}->[$j]->{font_size} if ( defined $args{row_props}->[$j]->{font_size} );
            }

            # Find out how wide this text will be, and whether it is greater
            # than the 'min_w' that may have been specified for the column.
            # !!! min_w?   Shouldn't this be max_w?
            #
            # Don't do this for images!
            if ( !$cell{image} ) {
                my $cellTextWidth = $self->{text}->advancewidth( $cell{text}, font => $cell{font}, fontsize => $cell{fontSize} );

                if ( $cellTextWidth > $width ) {
                    if ( !$maxWidth ) {
                        $width = $cellTextWidth;
                    } elsif ( $cellTextWidth > $maxWidth && !$cell{doNotTruncate} ) {

                        # !!! We're going to _truncate_ the string to fit.
                        # !!! For now, we'll attempt a somewhat peculiar 'center-truncation' technique.
                        #
                        my $elipsis = '...';
                        my $elipsisWidth = $self->{text}->advancewidth( $elipsis, font => $cell{font}, fontsize => $cell{fontSize} );

                        my @chars = split( //, $cell{text} );

                        my $preString;
                        my $postString;
                        my $lastPreString;
                        my $lastPostString;
                        while ( scalar @chars > 0 ) {
                            $preString .= shift(@chars);
                            if ( scalar @chars ) {
                                $postString = pop(@chars) . $postString;
                            }

                            my $preWidth  = $self->{text}->advancewidth( $preString,  font => $cell{font}, fontsize => $cell{fontSize} );
                            my $postWidth = $self->{text}->advancewidth( $postString, font => $cell{font}, fontsize => $cell{fontSize} );
                            if ( $preWidth + $postWidth + $elipsisWidth > $maxWidth ) {
                                last;
                            }
                            $lastPreString  = $preString;
                            $lastPostString = $postString;
                        }

                        # Replace text with truncated version.
                        #
                        $cell{text} = $lastPreString . $elipsis . $lastPostString;
                    }
                }
            }

            if ( $width > $collWidth[$i] ) {
                $collWidth[$i] = $width;
            }

            my $cellHeight = $cell{fontSize};

            # Do we need to increase the row height, too?
            #
            if ( $cellHeight > $rowHeight[$j] ) {
                $rowHeight[$j] = $cell{fontSize};
            }

            push @row, \%cell;
        }

        # If we have been asked to draw lines, take those into account too when calculating rowHeight
        #
        if ( defined $args{row_props} && defined $args{row_props}[$j]{lines} ) {
            my @lines;
            my $maxLineHeight = 0;
            foreach my $lineDef ( @{ $args{row_props}[$j]{lines} } ) {

                # !!! These defaults should be able to be globally overridden...
                #
                my $lineHeight = ( defined $lineDef->{pen_size} ? $lineDef->{pen_size} : kDefaultPenSize );
                $lineHeight += ( defined $lineDef->{top_pad}    ? $lineDef->{top_pad}    : kDefaultTopLinePad );
                $lineHeight += ( defined $lineDef->{bottom_pad} ? $lineDef->{bottom_pad} : kDefaultBottomLinePad );

                if ( $lineHeight > $maxLineHeight ) {
                    $maxLineHeight = $lineHeight;
                }

                push @lines, $lineDef;
            }

            $lineData{maxHeight} = $maxLineHeight;
            $lineData{lines}     = \@lines;
        }

        push @cells,    \@row;
        push @rowLines, \%lineData;
    }

    $self->{cells}       = \@cells;
    $self->{columnWidth} = \@collWidth;
    $self->{rowHeight}   = \@rowHeight;
    $self->{rowLines}    = \@rowLines;

    return $self;
}

# This method abstracts adjusting the 'Y' coordinate.
# It will instantiate a new page if necessary
#
sub adjustY {
    my ( $self, $y, $delta ) = @_;

    #    return $y unless defined $delta;

    if ( $y - $delta >= $self->{bottomMargin} ) {
        $y -= $delta;
        return $y;
    }

    my $funcRef = $self->{newPageFunc};

    $self->{page} = &$funcRef( $self->{pdf} );
    $self->{text} = $self->{page}->text();
    $self->{gfx}  = $self->{page}->gfx();
    $y            = $self->{newPageY};

    # Print out the 'repeat' row if there is one
    #
    if ( defined $self->{repeatRow} ) {
        $y = $self->printRow( $self->{_baseX}, $y, $self->{repeatRow} );
        $y -= $delta;
    } elsif ( defined $self->{repeatRows} ) {
        my @repeatRows = split( /-/, $self->{repeatRows} );
        shift(@repeatRows);
        foreach my $repeatRow (@repeatRows) {
            $y = $self->printRow( $self->{_baseX}, $y, $repeatRow );
        }
        $y -= $delta;
    }

    return $y;
}

sub wrapIfNecessary {
    my ( $self, $y, $delta ) = @_;

    if ( $y - $delta >= $self->{bottomMargin} ) {

        # Do nothing.
        #
        return $y;
    }

    my $funcRef = $self->{newPageFunc};

    $self->{page} = &$funcRef( $self->{pdf} );
    $self->{text} = $self->{page}->text();
    $self->{gfx}  = $self->{page}->gfx();
    $y            = $self->{newPageY};

    # Print out the 'repeat' row if there is one
    #
    if ( defined $self->{repeatRow} ) {
        $y = $self->printRow( $self->{_baseX}, $y, $self->{repeatRow} );
        $y -= $delta;
    } elsif ( defined $self->{repeatRows} ) {
        my @repeatRows = split( /-/, $self->{repeatRows} );
        shift(@repeatRows);
        foreach my $repeatRow (@repeatRows) {
            $y = $self->printRow( $self->{_baseX}, $y, $repeatRow );
        }
        $y -= $delta;
    }

    return $y;
}

sub printRow {
    my ( $self, $x, $y, $j ) = @_;

    my $localX = $x;

    my $clientNameClean = Common::Client::Current()->WebAlias();
    $clientNameClean = Common::Client::Current()->ClientNameClean() unless $clientNameClean;

    # Adjust 'y' down by the height of this row.
    #
    $y = $self->adjustY( $y, $self->{rowHeight}->[$j] + $self->{rowPadding}->[$j] );

    # In case we need to make a link later...
    #
    my $top = $y + $self->{rowHeight}->[$j] + $self->{rowPadding}->[$j];

    for ( my $i = 0 ; $i < $self->{numColumns} ; $i++ ) {
        my $cell     = $self->{cells}->[$j][$i];
        my $colWidth = $self->{columnWidth}->[$i];

        # Apply the cell padding to the left
        #
        $localX += $self->{columnPadding}->[$i];

        # For creating the link dimensions below.
        #
        my $left = $localX;

        # Set the font data appropriately
        #
        $self->{text}->font( $cell->{font}, $cell->{fontSize} );

        if ( $cell->{link} ) {
            $self->{text}->fillcolor("blue");
        } else {
            $self->{text}->fillcolor("black");
        }

        # Output the text
        #
        # Hold on, it could be an image!
        if ( $cell->{image} eq 'gif' ) {
            EmbedImage::addIconGIF(
                page   => $self->{page},
                pdf    => $self->{pdf},
                source => $cell->{text},
                link   => $cell->{link},
                x      => $x,
                y      => $y
            );

            # We create the link for images in the embed function, so let's clear it out now.
            #
            $cell->{link} = '';
        } elsif ( 'left' eq $cell->{justify} ) {
            $self->{text}->translate( $localX, $y );
            $self->{text}->text( $cell->{text} );
        } elsif ( 'right' eq $cell->{justify} ) {
            $self->{text}->translate( $localX + $colWidth, $y );
            $self->{text}->text_right( $cell->{text} );
        } elsif ( 'center' eq $cell->{justify} ) {
            $self->{text}->translate( $localX + ( $colWidth / 2 ), $y );
            $self->{text}->text_center( $cell->{text} );
        }

        $localX += $colWidth;

        if ( $cell->{link} ) {

            # And now we'll create the actual link
            my $annot  = $self->{page}->annotation();
            my $bottom = $y;
            my $right  = $localX;

            my $url = $cell->{link};
            my $anchorTest = substr( $url, 0, 1 );

            # !!!
            # If the first character of the link is '#',
            # we're going to assume this is meant to be an
            # internal link for this document.
            #

            if ( $anchorTest ne '#' ) {

                # external link
                $url = "https://" . $clientNameClean . ".royaltyshare.com" . $url;
                $annot->url( $url, -rect => [ $left, $bottom, $right, $top ] );
            } else {
                # internal link - strip '#' prefix for named_destination() call
                my $destName = substr( $url, 1 );
                my $dest = $self->{pdf}->named_destination( 'Dests', $destName );
                if ($dest && $dest->can('link')) {
                    $dest->link($self->{page});  # placeholder until album page is created
                }
                
                # Try destination object first (newer PDF::API2), fallback to string (older versions)
                eval {
                    $annot->link( $dest, -rect => [ $left, $bottom, $right, $top ] );
                    registerPendingLink( $self->{pdf}, $destName, $annot );

                    # Fix Action D: PDF::API2 doesn't set it correctly for named destinations
                    if ($annot->{A} && $annot->{A}->{S} && $annot->{A}->{S}->val eq 'GoTo' && $dest && $dest->{D}) {
                        $annot->{A}->{D} = $dest->{D};
                    }
                };
                if ($@) {
                    # Fallback: use destination name without '#' for compatibility with older PDF::API2
                    $annot->link( $destName, -rect => [ $left, $bottom, $right, $top ] );
                }
            }
        }

    }

    # Any lines to draw?
    #
    if ( defined $self->{rowLines}[$j] && defined $self->{rowLines}[$j]{maxHeight} ) {
        my $lineData  = $self->{rowLines}[$j];
        my $lineArray = $lineData->{lines};

        # Seems kinda dumb to draw a 'part' of a line on a page.
        # So, if the line height is going to cause the page to wrap, let's wrap the page first,
        # and _then_ start drawing the line.
        #
        $y = $self->wrapIfNecessary( $y, $lineData->{maxHeight} );

        foreach my $line (@$lineArray) {
            next unless ( defined $line->{start_col} && defined $line->{end_col} );
            my $lineXStart = $x;
            my $lineY      = $y;

            $lineY -= ( defined $line->{top_pad} ? $line->{top_pad} : kDefaultTopLinePad );

            # Find the starting X offset
            #
            for ( my $xx = 0 ; $xx < $line->{start_col} ; $xx++ ) {
                $lineXStart += $self->columnWidth($xx);
            }
            my $lineXEnd = $lineXStart + $self->columnWidth( $line->{start_col}, $line->{end_col} );

            my $lineHeight = ( defined $line->{pen_size} ? $line->{pen_size} : kDefaultPenSize );
            my $color      = ( defined $line->{color}    ? $line->{color}    : kDefaultLineColor );
            $self->{gfx}->strokecolor($color);

            # Draw the line
            #
            for ( my $yy = 0 ; $yy < $lineHeight ; $yy++ ) {
                $self->{gfx}->move( $lineXStart, $lineY );
                $self->{gfx}->line( $lineXEnd, $lineY );
                $self->{gfx}->stroke();

                $lineY--;
            }
        }

        $y = $self->adjustY( $y, $lineData->{maxHeight} );
    }

    return $y;
}

sub print {
    my ( $self, $x, $y ) = @_;

    $self->{_baseX} = $x;

    # Now the fun part: print this thing out.
    #
    for ( my $j = 0 ; $j < $self->{numRows} ; $j++ ) {
        $y = $self->printRow( $x, $y, $j );
    }

    return ( $self->{page}, $y );
}

sub columnWidth {
    my ( $self, $startCol, $endCol ) = @_;

    my $width = 0;

    $endCol = $startCol unless defined $endCol;

    for ( my $i = $startCol ; $i <= $endCol ; $i++ ) {
        $width += $self->{columnWidth}->[$i];
        $width += $self->{columnPadding}->[$i];
    }

    return $width;
}

sub rowHeight {
    my ( $self, $startRow, $endRow ) = @_;

    my $height = 0;

    $endRow = $startRow unless defined $endRow;

    for ( my $i = $startRow ; $i < $endRow ; $i++ ) {
        $height += $self->{rowHeight}->[$i];
        $height += $self->{rowPadding}->[$i];
    }

    return $height;
}

sub registerPendingLink {
    my ( $pdf, $destName, $annot ) = @_;

    return unless $pdf && $destName && $annot;
    $pdf->{_rs_pending_links} = {} unless $pdf->{_rs_pending_links};
    $pdf->{_rs_pending_links}{$destName} = [] unless $pdf->{_rs_pending_links}{$destName};
    push @{ $pdf->{_rs_pending_links}{$destName} }, $annot;
}

sub updatePendingLinks {
    my ( $pdf, $destName, $dest ) = @_;

    return unless $pdf && $destName && $dest && $dest->{D};
    my $pending = $pdf->{_rs_pending_links}{$destName};
    return unless $pending && ref($pending) eq 'ARRAY';

    foreach my $annot (@$pending) {
        next unless $annot->{A} && $annot->{A}->{S} && $annot->{A}->{S}->val eq 'GoTo';
        $annot->{A}->{D} = $dest->{D};
    }

    delete $pdf->{_rs_pending_links}{$destName};
}

1;
