# --
# Copyright (C) 2001-2021 OTRS AG, https://otrs.com/
# Copyright (C) 2021 Znuny GmbH, https://znuny.org/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --

package Kernel::Output::PDF::Ticket;

use strict;
use warnings;

our @ObjectDependencies = (
    'Kernel::Config',
    'Kernel::Output::HTML::Layout',
    'Kernel::System::CommunicationChannel',
    'Kernel::System::CustomerUser',
    'Kernel::System::DynamicField',
    'Kernel::System::DynamicField::Backend',
    'Kernel::System::LinkObject',
    'Kernel::System::Log',
    'Kernel::System::PDF',
    'Kernel::System::Ticket',
    'Kernel::System::Ticket::Article',
    'Kernel::System::User',
);

use Kernel::System::VariableCheck qw(:all);

sub new {
    my ( $Type, %Param ) = @_;

    # Allocate new hash for object.
    my $Self = {};
    bless( $Self, $Type );

    return $Self;
}

sub GeneratePDF {
    my ( $Self, %Param ) = @_;

    # Check needed params.
    for my $Needed (qw(TicketID UserID Interface)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => "error",
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    # Get appropriate interface flag.
    my %Interface;
    if ( $Param{Interface} eq 'Agent' ) {
        $Interface{Agent} = 1;
    }
    elsif ( $Param{Interface} eq 'Customer' ) {
        $Interface{Customer}             = 1;
        $Interface{IsVisibleForCustomer} = {
            IsVisibleForCustomer => 1,
        };
    }

    # Get ticket content.
    my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket');
    my %Ticket       = $TicketObject->TicketGet(
        TicketID      => $Param{TicketID},
        DynamicFields => 0,
        UserID        => $Param{UserID},
    );

    # Get article list.
    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
    my @MetaArticles  = $ArticleObject->ArticleList(
        TicketID => $Ticket{TicketID},
        UserID   => $Param{UserID},
        %{ $Interface{IsVisibleForCustomer} },
    );

    # Check if only one article should be printed in agent interface.
    if ( $Param{ArticleID} ) {
        @MetaArticles = grep { $_->{ArticleID} == $Param{ArticleID} } @MetaArticles;
    }

    # Get article content.
    my @ArticleBox;
    for my $MetaArticle (@MetaArticles) {
        my $ArticleBackendObject = $ArticleObject->BackendForArticle( %{$MetaArticle} );
        my %Article              = $ArticleBackendObject->ArticleGet(
            %{$MetaArticle},
            DynamicFields => 0,
        );
        my %Attachments = $ArticleBackendObject->ArticleAttachmentIndex(
            %{$MetaArticle},
            ExcludePlainText => 1,
            ExcludeHTMLBody  => 1,
            ExcludeInline    => 1,
        );
        $Article{Atms} = \%Attachments;
        push @ArticleBox, \%Article;
    }

    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
    my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
    my $LinkObject   = $Kernel::OM->Get('Kernel::System::LinkObject');

    # Get necessary data only for agent interface.
    my %TicketInfo;
    my %LinkData;
    if ( $Interface{Agent} ) {

        # Show total accounted time if feature is active.
        if ( $ConfigObject->Get('Ticket::Frontend::AccountTime') ) {
            $Ticket{TicketTimeUnits} = $TicketObject->TicketAccountedTimeGet(
                TicketID => $Ticket{TicketID},
            );
        }

        # Get user data.
        my $UserObject = $Kernel::OM->Get('Kernel::System::User');
        my %User       = $UserObject->GetUserData(
            UserID => $Param{UserID},
        );
        $TicketInfo{User} = \%User;

        # Get owner info.
        my %Owner = $UserObject->GetUserData(
            User => $Ticket{Owner},
        );
        $TicketInfo{Owner} = \%Owner;

        # Get responsible info.
        if ( $ConfigObject->Get('Ticket::Responsible') && $Ticket{Responsible} ) {
            my %Responsible = $UserObject->GetUserData(
                User => $Ticket{Responsible},
            );
            $TicketInfo{Responsible} = \%Responsible;
        }

        # Get link ticket data.
        my $LinkListWithData = $LinkObject->LinkListWithData(
            Object           => 'Ticket',
            Key              => $Param{TicketID},
            State            => 'Valid',
            UserID           => $Param{UserID},
            ObjectParameters => {
                Ticket => {
                    IgnoreLinkedTicketStateTypes => 1,
                },
            },
        );

        # Get the link data.
        if ( $LinkListWithData && ref $LinkListWithData eq 'HASH' && %{$LinkListWithData} ) {
            %LinkData = $LayoutObject->LinkObjectTableCreate(
                LinkListWithData => $LinkListWithData,
                ViewMode         => 'SimpleRaw',
            );
        }
    }

    # Get customer info.
    my $CustomerUserObject = $Kernel::OM->Get('Kernel::System::CustomerUser');
    my %CustomerData;
    if ( $Ticket{CustomerUserID} ) {
        %CustomerData = $CustomerUserObject->CustomerUserDataGet(
            User => $Ticket{CustomerUserID},
        );
    }
    elsif ( $Ticket{CustomerID} ) {
        %CustomerData = $CustomerUserObject->CustomerUserDataGet(
            CustomerID => $Ticket{CustomerID},
        );
    }

    # Transform time values into human readable form.
    $Ticket{Age} = $LayoutObject->CustomerAge(
        Age   => $Ticket{Age},
        Space => ' ',
    );

    if ( $Ticket{UntilTime} ) {
        $Ticket{PendingUntil} = $LayoutObject->CustomerAge(
            Age   => $Ticket{UntilTime},
            Space => ' ',
        );
    }

    # Get maximum number of pages.
    my %Page;
    $Page{MaxPages} = $ConfigObject->Get('PDF::MaxPages');
    if ( !$Page{MaxPages} || $Page{MaxPages} < 1 || $Page{MaxPages} > 1000 ) {
        $Page{MaxPages} = 100;
    }
    my $HeaderRight  = $ConfigObject->Get('Ticket::Hook') . $Ticket{TicketNumber};
    my $HeadlineLeft = $HeaderRight;
    my $Title        = $HeaderRight;
    if ( $Ticket{Title} ) {
        $HeadlineLeft = $Ticket{Title};
        $Title .= ' / ' . $Ticket{Title};
    }

    $Page{MarginTop}    = 30;
    $Page{MarginRight}  = 40;
    $Page{MarginBottom} = 40;
    $Page{MarginLeft}   = 40;
    $Page{HeaderRight}  = $HeaderRight;
    $Page{FooterLeft}   = '';
    $Page{PageText}     = $LayoutObject->{LanguageObject}->Translate('Page');
    $Page{PageCount}    = 1;

    # Create new PDF document.
    my $PDFObject = $Kernel::OM->Get('Kernel::System::PDF');
    $PDFObject->DocumentNew(
        Title  => $ConfigObject->Get('Product') . ': ' . $Title,
        Encode => $LayoutObject->{UserCharset},
    );

    # Create first PDF page.
    $PDFObject->PageNew(
        %Page,
        FooterRight => $Page{PageText} . ' ' . $Page{PageCount},
    );
    $Page{PageCount}++;

    $PDFObject->PositionSet(
        Move => 'relativ',
        Y    => -6,
    );

    # Output title.
    $PDFObject->Text(
        Text     => $Ticket{Title},
        FontSize => 13,
    );

    $PDFObject->PositionSet(
        Move => 'relativ',
        Y    => -6,
    );

    # Output "printed by".
    my $PrintedBy      = $LayoutObject->{LanguageObject}->Translate('printed by');
    my $DateTimeString = $Kernel::OM->Create('Kernel::System::DateTime')->ToString();
    my $Time           = $LayoutObject->{LanguageObject}->FormatTimeString(
        $DateTimeString,
        'DateFormat',
    );
    if ( $Interface{Agent} ) {
        $PrintedBy .= ' ' . $TicketInfo{User}{UserFullname} . ' ('
            . $TicketInfo{User}{UserEmail} . ')'
            . ', ' . $Time;
    }
    elsif ( $Interface{Customer} ) {
        $PrintedBy .= ' ' . $CustomerData{UserFullname} . ' ('
            . $CustomerData{UserEmail} . ')'
            . ', ' . $Time;
    }

    $PDFObject->Text(
        Text     => $PrintedBy,
        FontSize => 9,
    );

    $PDFObject->PositionSet(
        Move => 'relativ',
        Y    => -14,
    );

    # Output ticket infos.
    $Self->_PDFOutputTicketInfos(
        PageData        => \%Page,
        TicketData      => \%Ticket,
        OwnerData       => $TicketInfo{Owner},
        ResponsibleData => $TicketInfo{Responsible},
        Interface       => \%Interface,
    );

    $PDFObject->PositionSet(
        Move => 'relativ',
        Y    => -6,
    );

    # Output ticket dynamic fields.
    $Self->_PDFOutputTicketDynamicFields(
        PageData   => \%Page,
        TicketData => \%Ticket,
        Interface  => \%Interface,
    );

    $PDFObject->PositionSet(
        Move => 'relativ',
        Y    => -6,
    );

    # Output linked objects.
    if (%LinkData) {

        # Get link type list.
        my %LinkTypeList = $LinkObject->TypeList(
            UserID => $Param{UserID},
        );

        $Self->_PDFOutputLinkedObjects(
            PageData     => \%Page,
            LinkData     => \%LinkData,
            LinkTypeList => \%LinkTypeList,
        );
    }

    # Output customer infos.
    if (%CustomerData) {
        $Self->_PDFOutputCustomerInfos(
            PageData     => \%Page,
            CustomerData => \%CustomerData,
        );
    }

    # Output articles.
    $Self->_PDFOutputArticles(
        PageData      => \%Page,
        ArticleData   => \@ArticleBox,
        ArticleNumber => $Param{ArticleNumber},
        Interface     => \%Interface,
    );

    # Return the PDF document.
    return $PDFObject->DocumentOutput();
}

sub _PDFOutputTicketInfos {
    my ( $Self, %Param ) = @_;

    # Check needed stuff for both interfaces.
    for my $Needed (qw(PageData TicketData Interface)) {
        if ( !defined( $Param{$Needed} ) ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    # Check needed param for agent interface.
    if ( $Param{Interface}{Agent} && !defined( $Param{OwnerData} ) ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "Need OwnerData!"
        );
        return;
    }

    my %Ticket = %{ $Param{TicketData} };
    my %Page   = %{ $Param{PageData} };

    # Get needed objects.
    my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');

    # Create left table.
    my $TableLeft = [];
    my $CustomerConfig;
    if ( $Param{Interface}{Customer} ) {
        $CustomerConfig = $ConfigObject->Get("Ticket::Frontend::CustomerTicketZoom");
    }

    # Add ticket data, respecting AttributesView configuration for customer interface.
    for my $Attribute (qw(State Priority Queue Lock CustomerID)) {
        if ( $Param{Interface}{Agent} || $CustomerConfig->{AttributesView}->{$Attribute} ) {
            my $Row = {
                Key   => $LayoutObject->{LanguageObject}->Translate($Attribute),
                Value => $LayoutObject->{LanguageObject}->Translate( $Ticket{$Attribute} )
                    || $Ticket{$Attribute} || '-',
            };
            push( @{$TableLeft}, $Row );
        }
    }

    # Add Owner data, different output between interfaces.
    if ( $Param{Interface}{Agent} ) {
        my $OwnerRow = {
            Key   => $LayoutObject->{LanguageObject}->Translate('Owner'),
            Value => $Ticket{Owner} . ' (' . $Param{OwnerData}->{UserFullname} . ')',
        };
        push( @{$TableLeft}, $OwnerRow );
    }
    elsif ( $Param{Interface}{Customer} && $CustomerConfig->{AttributesView}->{Owner} ) {
        my $OwnerRow = {
            Key   => $LayoutObject->{LanguageObject}->Translate('Owner'),
            Value => $Ticket{Owner},
        };
        push( @{$TableLeft}, $OwnerRow );
    }

    # Add Responsible row, if feature is enabled.
    if ( $ConfigObject->Get('Ticket::Responsible') ) {
        my $Responsible = '-';
        if ( $Param{Interface}{Agent} && $Ticket{Responsible} ) {
            $Responsible = $Ticket{Responsible} . ' (' . $Param{ResponsibleData}->{UserFullname} . ')';
        }
        elsif (
            $Param{Interface}{Customer}
            && $CustomerConfig->{AttributesView}->{Responsible}
            && $Ticket{Responsible}
            )
        {
            $Responsible = $Ticket{Responsible};
        }
        my $Row = {
            Key   => $LayoutObject->{LanguageObject}->Translate('Responsible'),
            Value => $Responsible,
        };
        push( @{$TableLeft}, $Row );
    }

    # Add Type row, if feature is enabled.
    if (
        $ConfigObject->Get('Ticket::Type')
        && ( $Param{Interface}{Agent} || $CustomerConfig->{AttributesView}->{Type} )
        )
    {
        my $Row = {
            Key   => $LayoutObject->{LanguageObject}->Translate('Type'),
            Value => $Ticket{Type},
        };
        push( @{$TableLeft}, $Row );
    }

    # Add Service and SLA row, if feature is enabled.
    if ( $ConfigObject->Get('Ticket::Service') ) {
        if ( $Param{Interface}{Agent} || $CustomerConfig->{AttributesView}->{Service} ) {
            my $RowService = {
                Key   => $LayoutObject->{LanguageObject}->Translate('Service'),
                Value => $Ticket{Service} || '-',
            };
            push( @{$TableLeft}, $RowService );
        }
        if ( $Param{Interface}{Agent} || $CustomerConfig->{AttributesView}->{SLA} ) {
            my $RowSLA = {
                Key   => $LayoutObject->{LanguageObject}->Translate('SLA'),
                Value => $Ticket{SLA} || '-',
            };
            push( @{$TableLeft}, $RowSLA );
        }
    }

    # Create right table.
    my $TableRight = [
        {
            Key   => $LayoutObject->{LanguageObject}->Translate('Age'),
            Value => $LayoutObject->{LanguageObject}->Translate( $Ticket{Age} ),
        },
        {
            Key   => $LayoutObject->{LanguageObject}->Translate('Created'),
            Value => $LayoutObject->{LanguageObject}->FormatTimeString(
                $Ticket{Created},
                'DateFormat',
            ),
        },
    ];

    if ( $Param{Interface}{Customer} ) {
        unshift(
            @{$TableRight},
            {
                Key   => $LayoutObject->{LanguageObject}->Translate('CustomerID'),
                Value => $Ticket{CustomerID},
            }
        );
    }
    elsif ( $Param{Interface}{Agent} ) {

        # Show created by if different then User ID 1.
        if ( $Ticket{CreateBy} > 1 ) {
            my $Row = {
                Key   => $LayoutObject->{LanguageObject}->Translate('Created by'),
                Value => $Kernel::OM->Get('Kernel::System::User')->UserName( UserID => $Ticket{CreateBy} ),
            };
            push( @{$TableRight}, $Row );
        }

        # Show accounted time.
        if ( $ConfigObject->Get('Ticket::Frontend::AccountTime') ) {
            my $Row = {
                Key   => $LayoutObject->{LanguageObject}->Translate('Accounted time'),
                Value => $Ticket{TicketTimeUnits},
            };
            push( @{$TableRight}, $Row );
        }

        # Only show pending until unless it is really pending.
        if ( $Ticket{PendingUntil} ) {
            my $Row = {
                Key   => $LayoutObject->{LanguageObject}->Translate('Pending till'),
                Value => $Ticket{PendingUntil},
            };
            push( @{$TableRight}, $Row );
        }

        # Add first response time row.
        if ( defined( $Ticket{FirstResponseTime} ) ) {
            my $Row = {
                Key   => $LayoutObject->{LanguageObject}->Translate('First Response Time'),
                Value => $LayoutObject->{LanguageObject}->FormatTimeString(
                    $Ticket{FirstResponseTimeDestinationDate},
                    'DateFormat',
                    'NoSeconds',
                ),
            };
            push( @{$TableRight}, $Row );
        }

        # Add update time row.
        if ( defined( $Ticket{UpdateTime} ) ) {
            my $Row = {
                Key   => $LayoutObject->{LanguageObject}->Translate('Update Time'),
                Value => $LayoutObject->{LanguageObject}->FormatTimeString(
                    $Ticket{UpdateTimeDestinationDate},
                    'DateFormat',
                    'NoSeconds',
                ),
            };
            push( @{$TableRight}, $Row );
        }

        # Add solution time row.
        if ( defined( $Ticket{SolutionTime} ) ) {
            my $Row = {
                Key   => $LayoutObject->{LanguageObject}->Translate('Solution Time'),
                Value => $LayoutObject->{LanguageObject}->FormatTimeString(
                    $Ticket{SolutionTimeDestinationDate},
                    'DateFormat',
                    'NoSeconds',
                ),
            };
            push( @{$TableRight}, $Row );
        }
    }

    my $Rows = @{$TableLeft};
    if ( @{$TableRight} > $Rows ) {
        $Rows = @{$TableRight};
    }

    my %TableParam;
    for my $Row ( 1 .. $Rows ) {
        $Row--;
        $TableParam{CellData}[$Row][0]{Content}         = $TableLeft->[$Row]->{Key};
        $TableParam{CellData}[$Row][0]{Font}            = 'ProportionalBold';
        $TableParam{CellData}[$Row][1]{Content}         = $TableLeft->[$Row]->{Value};
        $TableParam{CellData}[$Row][2]{Content}         = ' ';
        $TableParam{CellData}[$Row][2]{BackgroundColor} = '#FFFFFF';
        $TableParam{CellData}[$Row][3]{Content}         = $TableRight->[$Row]->{Key};
        $TableParam{CellData}[$Row][3]{Font}            = 'ProportionalBold';
        $TableParam{CellData}[$Row][4]{Content}         = $TableRight->[$Row]->{Value};
    }

    $TableParam{ColumnData}[0]{Width} = 70;
    $TableParam{ColumnData}[1]{Width} = 156.5;
    $TableParam{ColumnData}[2]{Width} = 1;
    $TableParam{ColumnData}[3]{Width} = 70;
    $TableParam{ColumnData}[4]{Width} = 156.5;

    $TableParam{Type}                = 'Cut';
    $TableParam{Border}              = 0;
    $TableParam{FontSize}            = 7;
    $TableParam{BackgroundColorEven} = '#F2F2F2';
    $TableParam{Padding}             = 6;
    $TableParam{PaddingTop}          = 3;
    $TableParam{PaddingBottom}       = 3;

    # Output table.
    my $PDFObject = $Kernel::OM->Get('Kernel::System::PDF');
    PAGE:
    for ( $Page{PageCount} .. $Page{MaxPages} ) {

        # Output table (or a fragment of it).
        %TableParam = $PDFObject->Table( %TableParam, );

        # Stop output or output next page.
        if ( $TableParam{State} ) {
            last PAGE;
        }
        else {
            $PDFObject->PageNew(
                %Page,
                FooterRight => $Page{PageText} . ' ' . $Page{PageCount},
            );
            $Page{PageCount}++;
        }
    }
    return 1;
}

sub _PDFOutputLinkedObjects {
    my ( $Self, %Param ) = @_;

    # Check needed stuff.
    for my $Needed (qw(PageData LinkData LinkTypeList)) {
        if ( !defined( $Param{$Needed} ) ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    my %Page     = %{ $Param{PageData} };
    my %TypeList = %{ $Param{LinkTypeList} };
    my %TableParam;
    my $Row = 0;

    # Get needed objects.
    my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
    my $PDFObject    = $Kernel::OM->Get('Kernel::System::PDF');

    for my $LinkTypeLinkDirection ( sort { lc $a cmp lc $b } keys %{ $Param{LinkData} } ) {

        # Investigate link type name.
        my @LinkData     = split q{::}, $LinkTypeLinkDirection;
        my $LinkTypeName = $TypeList{ $LinkData[0] }->{ $LinkData[1] . 'Name' };
        $LinkTypeName = $LayoutObject->{LanguageObject}->Translate($LinkTypeName);

        # Define headline.
        $TableParam{CellData}[$Row][0]{Content} = $LinkTypeName . ':';
        $TableParam{CellData}[$Row][0]{Font}    = 'ProportionalBold';
        $TableParam{CellData}[$Row][1]{Content} = '';

        # Extract object list.
        my $ObjectList = $Param{LinkData}->{$LinkTypeLinkDirection};

        for my $Object ( sort { lc $a cmp lc $b } keys %{$ObjectList} ) {

            for my $Item ( @{ $ObjectList->{$Object} } ) {

                $TableParam{CellData}[$Row][0]{Content} ||= '';
                $TableParam{CellData}[$Row][1]{Content} = $Item->{Title} || '';
            }
            continue {
                $Row++;
            }
        }
    }

    $TableParam{ColumnData}[0]{Width} = 80;
    $TableParam{ColumnData}[1]{Width} = 431;

    $PDFObject->HLine(
        Color     => '#aaa',
        LineWidth => 0.5,
    );

    # Set new position.
    $PDFObject->PositionSet(
        Move => 'relativ',
        Y    => -10,
    );

    # Output headline.
    $PDFObject->Text(
        Text     => $LayoutObject->{LanguageObject}->Translate('Linked Objects'),
        Height   => 10,
        Type     => 'Cut',
        Font     => 'Proportional',
        FontSize => 9,
        Color    => '#666666',
    );

    # Set new position.
    $PDFObject->PositionSet(
        Move => 'relativ',
        Y    => -4,
    );

    # Table params.
    $TableParam{Type}          = 'Cut';
    $TableParam{Border}        = 0;
    $TableParam{FontSize}      = 6;
    $TableParam{Padding}       = 1;
    $TableParam{PaddingTop}    = 3;
    $TableParam{PaddingBottom} = 3;

    # Output table.
    PAGE:
    for ( $Page{PageCount} .. $Page{MaxPages} ) {

        # Output table (or a fragment of it).
        %TableParam = $PDFObject->Table( %TableParam, );

        # Stop output or output next page.
        if ( $TableParam{State} ) {
            last PAGE;
        }
        else {
            $PDFObject->PageNew(
                %Page,
                FooterRight => $Page{PageText} . ' ' . $Page{PageCount},
            );
            $Page{PageCount}++;
        }
    }

    return 1;
}

sub _PDFOutputTicketDynamicFields {
    my ( $Self, %Param ) = @_;

    # Check needed stuff.
    for my $Needed (qw(PageData TicketData)) {
        if ( !defined( $Param{$Needed} ) ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }
    my $Output = 0;
    my %Ticket = %{ $Param{TicketData} };
    my %Page   = %{ $Param{PageData} };

    my %TableParam;
    my $Row = 0;

    # Get dynamic field config for appropriate frontend module.
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
    my $DynamicFieldFilter;
    if ( $Param{Interface}{Agent} ) {
        $DynamicFieldFilter = $ConfigObject->Get("Ticket::Frontend::AgentTicketPrint")->{DynamicField};
    }
    elsif ( $Param{Interface}{Customer} ) {
        $DynamicFieldFilter = $ConfigObject->Get("Ticket::Frontend::CustomerTicketPrint")->{DynamicField};
    }

    # Get the dynamic fields for ticket object.
    my $DynamicField = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet(
        Valid       => 1,
        ObjectType  => ['Ticket'],
        FieldFilter => $DynamicFieldFilter || {},
    );

    # Get needed objects.
    my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');
    my $LayoutObject              = $Kernel::OM->Get('Kernel::Output::HTML::Layout');

    # Generate table, cycle trough the activated Dynamic Fields for ticket object.
    DYNAMICFIELD:
    for my $DynamicFieldConfig ( @{$DynamicField} ) {
        next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);

        if ( $Param{Interface}{Customer} ) {

            # Skip dynamic field if is not designed for customer interface.
            my $IsCustomerInterfaceCapable = $DynamicFieldBackendObject->HasBehavior(
                DynamicFieldConfig => $DynamicFieldConfig,
                Behavior           => 'IsCustomerInterfaceCapable',
            );
            next DYNAMICFIELD if !$IsCustomerInterfaceCapable;
        }

        my $Value = $DynamicFieldBackendObject->ValueGet(
            DynamicFieldConfig => $DynamicFieldConfig,
            ObjectID           => $Ticket{TicketID},
        );

        next DYNAMICFIELD if !$Value;
        next DYNAMICFIELD if $Value eq "";

        # Get print string for this dynamic field.
        my $ValueStrg = $DynamicFieldBackendObject->DisplayValueRender(
            DynamicFieldConfig => $DynamicFieldConfig,
            Value              => $Value,
            HTMLOutput         => 0,
            LayoutObject       => $LayoutObject,
        );

        $TableParam{CellData}[$Row][0]{Content}
            = $LayoutObject->{LanguageObject}->Translate( $DynamicFieldConfig->{Label} )
            . ':';
        $TableParam{CellData}[$Row][0]{Font}    = 'ProportionalBold';
        $TableParam{CellData}[$Row][1]{Content} = $ValueStrg->{Value};

        $Row++;
        $Output = 1;
    }

    $TableParam{ColumnData}[0]{Width} = 80;
    $TableParam{ColumnData}[1]{Width} = 431;

    # Output ticket dynamic fields.
    if ($Output) {

        my $PDFObject = $Kernel::OM->Get('Kernel::System::PDF');

        $PDFObject->HLine(
            Color     => '#aaa',
            LineWidth => 0.5,
        );

        # Set new position.
        $PDFObject->PositionSet(
            Move => 'relativ',
            Y    => -10,
        );

        # Output headline.
        $PDFObject->Text(
            Text     => $LayoutObject->{LanguageObject}->Translate('Ticket Dynamic Fields'),
            Height   => 10,
            Type     => 'Cut',
            Font     => 'Proportional',
            FontSize => 8,
            Color    => '#666666',
        );

        # Set new position.
        $PDFObject->PositionSet(
            Move => 'relativ',
            Y    => -4,
        );

        # Table params.
        $TableParam{Type}          = 'Cut';
        $TableParam{Border}        = 0;
        $TableParam{FontSize}      = 6;
        $TableParam{Padding}       = 1;
        $TableParam{PaddingTop}    = 3;
        $TableParam{PaddingBottom} = 3;

        # Output table.
        PAGE:
        for ( $Page{PageCount} .. $Page{MaxPages} ) {

            # Output table (or a fragment of it).
            %TableParam = $PDFObject->Table( %TableParam, );

            # Stop output or output next page.
            if ( $TableParam{State} ) {
                last PAGE;
            }
            else {
                $PDFObject->PageNew(
                    %Page,
                    FooterRight => $Page{PageText} . ' ' . $Page{PageCount},
                );
                $Page{PageCount}++;
            }
        }
    }
    return 1;
}

sub _PDFOutputCustomerInfos {
    my ( $Self, %Param ) = @_;

    my $LayoutObject              = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
    my $DynamicFieldObject        = $Kernel::OM->Get('Kernel::System::DynamicField');
    my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');

    for my $Needed (qw(PageData CustomerData)) {
        if ( !defined( $Param{$Needed} ) ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }
    my $Output       = 0;
    my %CustomerData = %{ $Param{CustomerData} };
    my %Page         = %{ $Param{PageData} };
    my %TableParam;
    my $Row = 0;
    my $Map = $CustomerData{Config}->{Map};

    # Check if customer company support is enabled.
    if ( $CustomerData{Config}->{CustomerCompanySupport} ) {
        my $Map2 = $CustomerData{CompanyConfig}->{Map};
        if ($Map2) {
            push( @{$Map}, @{$Map2} );
        }
    }

    FIELD:
    for my $Field ( @{$Map} ) {
        next FIELD if !defined $CustomerData{ $Field->[0] };
        next FIELD if !length $CustomerData{ $Field->[0] };
        next FIELD if !$Field->[3];                            # not visible

        my $Label = $LayoutObject->{LanguageObject}->Translate( $Field->[1] ) . ':';
        my $Value = $CustomerData{ $Field->[0] };

        # render dynamic field values
        if ( $Field->[5] eq 'dynamic_field' ) {
            next FIELD if $Field->[0] !~ m{\ADynamicField_(.*)\z};

            my $DynamicFieldName   = $1;
            my $DynamicFieldConfig = $DynamicFieldObject->DynamicFieldGet(
                Name => $DynamicFieldName,
            );
            next FIELD if !IsHashRefWithData($DynamicFieldConfig);

            my $Values = $Value;
            if ( !IsArrayRefWithData($Values) ) {
                $Values = [$Value];
            }

            my @RenderedValues;
            VALUE:
            for my $Value ( @{$Values} ) {
                my $RenderedValue = $DynamicFieldBackendObject->DisplayValueRender(
                    DynamicFieldConfig => $DynamicFieldConfig,
                    Value              => $Value,
                    HTMLOutput         => 0,
                    LayoutObject       => $LayoutObject,
                );

                next VALUE if !IsHashRefWithData($RenderedValue) || !defined $RenderedValue->{Value};

                push @RenderedValues, $RenderedValue->{Value};
            }

            $Value = join ', ', @RenderedValues;
            $Label = $DynamicFieldConfig->{Label};
        }

        $TableParam{CellData}[$Row][0]{Font}    = 'ProportionalBold';
        $TableParam{CellData}[$Row][0]{Content} = $Label;
        $TableParam{CellData}[$Row][1]{Content} = $Value;

        $Row++;
        $Output = 1;
    }
    $TableParam{ColumnData}[0]{Width} = 80;
    $TableParam{ColumnData}[1]{Width} = 431;

    if ($Output) {

        my $PDFObject = $Kernel::OM->Get('Kernel::System::PDF');

        # Set new position.
        $PDFObject->PositionSet(
            Move => 'relativ',
            Y    => -10,
        );

        $PDFObject->HLine(
            Color     => '#aaa',
            LineWidth => 0.5,
        );

        # Set new position.
        $PDFObject->PositionSet(
            Move => 'relativ',
            Y    => -4,
        );

        # Output headline.
        $PDFObject->Text(
            Text     => $LayoutObject->{LanguageObject}->Translate('Customer Information'),
            Height   => 10,
            Type     => 'Cut',
            Font     => 'Proportional',
            FontSize => 8,
            Color    => '#666666',
        );

        # Set new position.
        $PDFObject->PositionSet(
            Move => 'relativ',
            Y    => -4,
        );

        # Table params.
        $TableParam{Type}          = 'Cut';
        $TableParam{Border}        = 0;
        $TableParam{FontSize}      = 6;
        $TableParam{Padding}       = 1;
        $TableParam{PaddingTop}    = 3;
        $TableParam{PaddingBottom} = 3;

        # Output table.
        PAGE:
        for ( $Page{PageCount} .. $Page{MaxPages} ) {

            # Output table (or a fragment of it).
            %TableParam = $PDFObject->Table( %TableParam, );

            # Stop output or output next page.
            if ( $TableParam{State} ) {
                last PAGE;
            }
            else {
                $PDFObject->PageNew(
                    %Page,
                    FooterRight => $Page{PageText} . ' ' . $Page{PageCount},
                );
                $Page{PageCount}++;
            }
        }
    }
    return 1;
}

sub _PDFOutputArticles {
    my ( $Self, %Param ) = @_;

    # Check needed stuff.
    for my $Needed (qw(PageData ArticleData)) {
        if ( !defined( $Param{$Needed} ) ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }
    my %Page = %{ $Param{PageData} };

    # Get needed objects.
    my $PDFObject                 = $Kernel::OM->Get('Kernel::System::PDF');
    my $ArticleObject             = $Kernel::OM->Get('Kernel::System::Ticket::Article');
    my $LayoutObject              = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
    my $ConfigObject              = $Kernel::OM->Get('Kernel::Config');
    my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');

    my @ArticleData  = @{ $Param{ArticleData} };
    my $ArticleCount = scalar @ArticleData;

    # Get config settings.
    my $ZoomExpandSort = $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Frontend::ZoomExpandSort');

    # Resort article order.
    if ( $Param{Interface}{Agent} && $ZoomExpandSort eq 'reverse' ) {
        @ArticleData = reverse(@ArticleData);
    }

    my $ArticleCounter = 1;
    for my $ArticleTmp (@ArticleData) {

        my %Article = %{$ArticleTmp};

        # Get attachment string.
        my %AtmIndex = ();
        if ( $Article{Atms} ) {
            %AtmIndex = %{ $Article{Atms} };
        }
        my $Attachments;
        for my $FileID ( sort keys %AtmIndex ) {
            my %File     = %{ $AtmIndex{$FileID} };
            my $Filesize = $LayoutObject->HumanReadableDataSize( Size => $File{FilesizeRaw} );
            $Attachments .= $File{Filename} . ' (' . $Filesize . ")\n";
        }

        # Show total accounted time if feature is active.
        if ( $Param{Interface}{Agent} && $ConfigObject->Get('Ticket::Frontend::AccountTime') ) {
            $Article{'Accounted time'} = $ArticleObject->ArticleAccountedTimeGet(
                ArticleID => $Article{ArticleID},
            );
        }

        # Generate article info table.
        my %TableParam1;
        my $Row = 0;

        $PDFObject->PositionSet(
            Move => 'relativ',
            Y    => -6,
        );

        # Get article number.
        my $ArticleNumber;
        if ( $Param{ArticleNumber} ) {
            $ArticleNumber = $Param{ArticleNumber};
        }
        else {
            $ArticleNumber = $ZoomExpandSort eq 'reverse' ? $ArticleCount - $ArticleCounter + 1 : $ArticleCounter;
        }

        if ( $Param{Interface}{Customer} ) {
            $ArticleNumber = $ArticleCounter;
        }

        # Article number tag.
        $PDFObject->Text(
            Text     => $LayoutObject->{LanguageObject}->Translate('Article') . ' #' . $ArticleNumber,
            Height   => 10,
            Type     => 'Cut',
            Font     => 'Proportional',
            FontSize => 8,
            Color    => '#666666',
        );

        $PDFObject->PositionSet(
            Move => 'relativ',
            Y    => 2,
        );

        my @CommunicationChannelList = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelList();
        my %CommunicationChannels    = map { $_->{ChannelID} => $_->{ChannelName} } @CommunicationChannelList;

        my %ArticleFields = $LayoutObject->ArticleFields(%Article);

        # Display article fields.
        ARTICLE_FIELD:
        for my $ArticleFieldKey (
            sort { $ArticleFields{$a}->{Prio} <=> $ArticleFields{$b}->{Prio} }
            keys %ArticleFields
            )
        {
            my %ArticleField = %{ $ArticleFields{$ArticleFieldKey} // {} };

            next ARTICLE_FIELD if $Param{Interface}->{Customer} && $ArticleField{HideInCustomerInterface};
            next ARTICLE_FIELD if $ArticleField{HideInTicketPrint};
            next ARTICLE_FIELD if !$ArticleField{Value};

            my $FieldValue = $ArticleField{Value};

            if (
                $CommunicationChannels{ $Article{CommunicationChannelID} } eq 'Internal'
                && $Article{SenderType} eq 'agent'
                && $Param{Interface}->{Customer}
                && $ArticleFieldKey eq 'From'
                )
            {

                my $DisplayNoteFrom = $ConfigObject->Get('Ticket::Frontend::CustomerTicketZoom')->{DisplayNoteFrom}
                    || '';

                if ( $DisplayNoteFrom && $DisplayNoteFrom eq 'DefaultAgentName' ) {
                    $FieldValue = $ConfigObject->Get('Ticket::Frontend::CustomerTicketZoom')->{DefaultAgentName}
                        || 'Support Agent';
                }
            }

            $TableParam1{CellData}[$Row][0]{Content}
                = $LayoutObject->{LanguageObject}->Translate( $ArticleField{Label} ) . ':';
            $TableParam1{CellData}[$Row][0]{Font}    = 'ProportionalBold';
            $TableParam1{CellData}[$Row][1]{Content} = $FieldValue;
            $Row++;
        }

        # Display article accounted time.
        if ( $Param{Interface}{Agent} ) {
            my $ArticleTime = $ArticleObject->ArticleAccountedTimeGet(
                ArticleID => $Article{ArticleID},
            );
            if ($ArticleTime) {
                $TableParam1{CellData}[$Row][0]{Content}
                    = $LayoutObject->{LanguageObject}->Translate('Accounted time') . ':';
                $TableParam1{CellData}[$Row][0]{Font}    = 'ProportionalBold';
                $TableParam1{CellData}[$Row][1]{Content} = $ArticleTime;
                $Row++;
            }
        }

        $TableParam1{CellData}[$Row][0]{Content} = $LayoutObject->{LanguageObject}->Translate('Created') . ':';
        $TableParam1{CellData}[$Row][0]{Font}    = 'ProportionalBold';
        $TableParam1{CellData}[$Row][1]{Content} = $LayoutObject->{LanguageObject}->FormatTimeString(
            $Article{CreateTime},
            'DateFormat',
        );
        $TableParam1{CellData}[$Row][1]{Content}
            .= ' ' . $LayoutObject->{LanguageObject}->Translate('by');
        my $SenderType = $ArticleObject->ArticleSenderTypeLookup(
            SenderTypeID => $Article{SenderTypeID},
        );
        $TableParam1{CellData}[$Row][1]{Content}
            .= ' ' . $LayoutObject->{LanguageObject}->Translate($SenderType);
        $Row++;

        # Get dynamic field config for appropriate frontend module.
        my $DynamicFieldFilter;
        if ( $Param{Interface}{Agent} ) {
            $DynamicFieldFilter = $ConfigObject->Get("Ticket::Frontend::AgentTicketPrint")->{DynamicField};
        }
        elsif ( $Param{Interface}{Customer} ) {
            $DynamicFieldFilter = $ConfigObject->Get("Ticket::Frontend::CustomerTicketPrint")->{DynamicField};
        }

        # Get the dynamic fields for ticket object.
        my $DynamicField = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet(
            Valid       => 1,
            ObjectType  => ['Article'],
            FieldFilter => $DynamicFieldFilter || {},
        );

        # Generate table, cycle trough the activated Dynamic Fields for ticket object.
        DYNAMICFIELD:
        for my $DynamicFieldConfig ( @{$DynamicField} ) {
            next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);

            if ( $Param{Interface}{Customer} ) {

                # Skip the dynamic field if it is not designed for customer interface.
                my $IsCustomerInterfaceCapable = $DynamicFieldBackendObject->HasBehavior(
                    DynamicFieldConfig => $DynamicFieldConfig,
                    Behavior           => 'IsCustomerInterfaceCapable',
                );
                next DYNAMICFIELD if !$IsCustomerInterfaceCapable;
            }

            my $Value = $DynamicFieldBackendObject->ValueGet(
                DynamicFieldConfig => $DynamicFieldConfig,
                ObjectID           => $Article{ArticleID},
            );

            next DYNAMICFIELD if !$Value;
            next DYNAMICFIELD if $Value eq "";

            # Get print string for this dynamic field.
            my $ValueStrg = $DynamicFieldBackendObject->DisplayValueRender(
                DynamicFieldConfig => $DynamicFieldConfig,
                Value              => $Value,
                HTMLOutput         => 0,
                LayoutObject       => $LayoutObject,
            );
            $TableParam1{CellData}[$Row][0]{Content}
                = $LayoutObject->{LanguageObject}->Translate( $DynamicFieldConfig->{Label} )
                . ':';
            $TableParam1{CellData}[$Row][0]{Font}    = 'ProportionalBold';
            $TableParam1{CellData}[$Row][1]{Content} = $ValueStrg->{Value};
            $Row++;
        }

        if ($Attachments) {
            $TableParam1{CellData}[$Row][0]{Content} = $LayoutObject->{LanguageObject}->Translate('Attachment') . ':';
            $TableParam1{CellData}[$Row][0]{Font}    = 'ProportionalBold';
            chomp($Attachments);
            $TableParam1{CellData}[$Row][1]{Content} = $Attachments;
        }
        $TableParam1{ColumnData}[0]{Width} = 80;
        $TableParam1{ColumnData}[1]{Width} = 431;

        $PDFObject->PositionSet(
            Move => 'relativ',
            Y    => -6,
        );

        $PDFObject->HLine(
            Color     => '#aaa',
            LineWidth => 0.5,
        );

        $PDFObject->PositionSet(
            Move => 'relativ',
            Y    => -6,
        );

        # Table params (article infos).
        $TableParam1{Type}          = 'Cut';
        $TableParam1{Border}        = 0;
        $TableParam1{FontSize}      = 6;
        $TableParam1{Padding}       = 1;
        $TableParam1{PaddingTop}    = 3;
        $TableParam1{PaddingBottom} = 3;

        # Output table (article infos).
        PAGE:
        for ( $Page{PageCount} .. $Page{MaxPages} ) {

            # Output table (or a fragment of it).
            %TableParam1 = $PDFObject->Table( %TableParam1, );

            # Stop output or output next page.
            if ( $TableParam1{State} ) {
                last PAGE;
            }
            else {
                $PDFObject->PageNew(
                    %Page,
                    FooterRight => $Page{PageText} . ' ' . $Page{PageCount},
                );
                $Page{PageCount}++;
            }
        }

        my $ArticlePreview = $LayoutObject->ArticlePreview(
            %Article,
            ResultType => 'plain',
        );

        # Table params (article body).
        my %TableParam2;
        $TableParam2{CellData}[0][0]{Content} = $ArticlePreview || ' ';
        $TableParam2{Type}                    = 'Cut';
        $TableParam2{Border}                  = 0;
        $TableParam2{Font}                    = 'Monospaced';
        $TableParam2{FontSize}                = 7;
        $TableParam2{BackgroundColor}         = '#f2f2f2';
        $TableParam2{Padding}                 = 4;
        $TableParam2{PaddingTop}              = 4;
        $TableParam2{PaddingBottom}           = 4;

        # Output table (article body).
        PAGE:
        for ( $Page{PageCount} .. $Page{MaxPages} ) {

            # Output table (or a fragment of it).
            %TableParam2 = $PDFObject->Table( %TableParam2, );

            # Stop output or output next page.
            if ( $TableParam2{State} ) {
                last PAGE;
            }
            else {
                $PDFObject->PageNew(
                    %Page,
                    FooterRight => $Page{PageText} . ' ' . $Page{PageCount},
                );
                $Page{PageCount}++;
            }
        }
        $ArticleCounter++;
    }
    return 1;
}

1;
