
package LedgerSMB::Template::Plugin::ODS;

=head1 NAME

LedgerSMB::Template::Plugin::ODS - Template support module for LedgerSMB

=head1 DESCRIPTION

Implements C<LedgerSMB::Template>'s FORMATTER protocol for ODS output.

=cut

use strict;
use warnings;

use Data::Dumper;  ## no critic
use XML::Twig;
use Digest::MD5 qw(md5_hex);
use File::Temp;
use OpenOffice::OODoc;
use OpenOffice::OODoc::Styles;


use Moo;

=head1 ATTRIBUTES

=head2 formats

Holds an array of strings naming the formats supported by this plugin.

=cut

has formats => (is => 'ro', default => sub { [ 'ODS' ] });

=head2 format

Holds a string naming the actual format for which this plugin
is configured. The plugin can be used multiple times with different
formats, as long as they are in the list of formats.

=cut

has format => (is => 'ro', default => 'ODS');


# SC: The ODS handlers need these vars in common
my $ods;
my $rowcount;
my $currcol;
my $maxrows;
my $maxcols;
my %celltype;
my $sheetnum = -1;
my $sheetname;

# SC: The elements of the style table for regular styles and stack are
#     arrays where the stack name is the first element and the style
#     properties are the second.  The name is used for setting styles,
#     while the properties are used in handling nested styles.
my @style_stack;    # stack of styles, 0 is active style
my %style_table;    # hash table for created styles

# SC: Subtract 8 from the attribute to get the index
#     http://search.cpan.org/src/JMCNAMARA/Spreadsheet-WriteExcel-2.11/doc/palette.html
my @colour = (odfColor(0, 0, 0), odfColor(255, 255, 255),  ## no critic (ProhibitMagicNumbers) sniff
    odfColor(255, 0, 0), odfColor(0, 255, 0),  ## no critic (ProhibitMagicNumbers) sniff
    odfColor(0, 0, 255), odfColor(255, 255, 0),  ## no critic (ProhibitMagicNumbers) sniff
    odfColor(255, 0, 255), odfColor(0, 255, 255),  ## no critic (ProhibitMagicNumbers) sniff
    odfColor(128, 0, 0), odfColor(0, 128, 0),
    odfColor(0, 0, 128), odfColor(128, 128, 0),
    odfColor(128, 0, 128), odfColor(0, 128, 128),
    odfColor(192, 192, 192), odfColor(128, 128, 128),
    odfColor(153, 153, 255), odfColor(153, 51, 102),
    odfColor(255, 255, 204), odfColor(204, 255, 255),
    odfColor(102, 0, 102), odfColor(255, 128, 128),
    odfColor(0, 102, 204), odfColor(204, 204, 255),
    odfColor(0, 0, 128), odfColor(255, 0, 255),
    odfColor(255, 255, 0), odfColor(0, 255, 255),
    odfColor(128, 0, 128), odfColor(120, 0, 0),
    odfColor(0, 128, 128), odfColor(0, 0, 255),
    odfColor(0, 204, 255), odfColor(204, 255, 255),
    odfColor(204, 255, 204), odfColor(255, 255, 153),
    odfColor(153, 204, 255), odfColor(255, 153, 204),
    odfColor(204, 153, 255), odfColor(192, 192, 192),
    odfColor(51, 102, 255), odfColor(51, 204, 204),
    odfColor(153, 204, 0), odfColor(255, 204, 0),
    odfColor(255, 153, 0), odfColor(255, 102, 0),
    odfColor(102, 102, 153), odfColor(150, 150, 150),
    odfColor(0, 51, 102), odfColor(51, 153, 102),
    odfColor(0, 51, 0), odfColor(51, 51, 0),
    odfColor(153, 51, 0), odfColor(153, 51, 102),
    odfColor(51, 51, 153), odfColor(51, 51, 51),
    );
my %colour_name = ('black' => $colour[0], 'white' => $colour[1],
    'red' => $colour[2], 'lime' => $colour[3],
    'blue' => $colour[4], 'yellow' => $colour[5],
    'magenta' => $colour[6], 'cyan' => $colour[7],
    'brown' => $colour[8], 'green' => $colour[9],
    'navy' => $colour[10], 'purple' => $colour[12],
    'silver' => $colour[14], 'gray' => $colour[15],
    'grey' => $colour[15], 'orange' => $colour[45],
    );

my @line_width = ('none', '0.018cm solid', '0.035cm solid',
    '0.018cm dashed', '0.018cm dotted', '0.141cm solid',
    '0.039cm double', '0.002cm solid'
    );

sub _worksheet_handler {
    $sheetnum += 1;
    $rowcount = -1;
    $currcol = 0;
    my $rows = $_->{att}->{rows};
    my $columns = $_->{att}->{columns};
    $rows ||= 1000;
    $columns ||= 52;
    $maxrows = $rows;
    $maxcols = $columns;
    my $sheet;
    if ($_->is_first_child) {
        # Rename "Sheet1" to whatever we want to be our first sheet
        $sheet = $ods->getTable(0, $rows, $columns);
        $ods->renameTable($sheet, $_->{att}->{name});
        $sheetname = $_->{att}->{name};
    } else {
        $sheet = $ods->appendTable($_->{att}->{name}, $rows, $columns);
        $sheetname = $_->{att}->{name};
    }
    return undef;
}

sub _row_handler {
    $rowcount++;
    $currcol = 0;
    return undef;
}

sub _cell_handler {
    $ods->expandTable($sheetname, $maxrows, $maxcols);
    my $cell = $ods->getCell($sheetname, $rowcount, $currcol);

    if (@style_stack and $celltype{$style_stack[0][0]}) {
        $ods->cellValueType($cell, $celltype{$style_stack[0][0]}[0]);
    } elsif ($_->{att}->{type}) {
        my $type = $_->{att}->{type};
        if ($type =~ /^(string|blank|url)$/i) {
            $ods->cellValueType($cell, 'string');
        } elsif ($type =~ /^(number|formula)$/i) {
            $ods->cellValueType($cell, 'float');
        }
    }
    $ods->cellValue($sheetname, $rowcount, $currcol, $_->{att}->{text});
    if (@style_stack) {
        $ods->cellStyle($cell, $style_stack[0][0]);
    }
    $currcol++;
    return undef;
}

sub _formula_handler {
    my $cell = $ods->getCell($sheetnum, $rowcount, $currcol);

    if (@style_stack and $celltype{$style_stack[0][0]}) {
        $ods->cellValueType($cell, $celltype{$style_stack[0][0]}[0]);
    } elsif ($_->{att}->{type}) {
        my $type = $_->{att}->{type};
        if ($type =~ /^(string|blank|url)$/i) {
            $ods->cellValueType($cell, 'string');
        } elsif ($type =~ /^(number|formula)$/i) {
            $ods->cellValueType($cell, 'float');
        }
    }
    $ods->cellFormula($cell, "oooc:=$_->{att}->{text}");
    if (@style_stack) {
        $ods->cellStyle($cell, $style_stack[0][0]);
    }
    ++$currcol;
    return undef;
}

sub _border_set {
    my ($format, $properties, $border) = @_;
    my $edge = $border;
    $edge =~ s/^border-?//;

    my $val;
    if ($edge) {
        $val = $format->{att}{$edge};
    } else {
        $val = $format->{att}{'border'};
    }

    if ($properties->{cell}{"fo:$border"}){
        $properties->{cell}{"fo:$border"} =~ s/^.* (\#......)$/$val $1/;
    } else {
        $properties->{cell}{"fo:$border"} = "$line_width[$val] #000000";
    }

    my $colour = $format->{att}->{"${edge}_color"};
    if ($colour =~ /^\d+$/) {
        $colour = $colour[$colour];
    } elsif ($colour !~ /^\#......$/) {
        $colour = $colour_name{$colour};
    }
    if ($edge and $format->{att}->{"${edge}_color"}) {
        return $properties->{cell}{"fo:$border"} =~ s/^(.*) \#......$/$1 $colour/;
    } elsif ($format->{att}->{border_color}) {
        return $properties->{cell}{"fo:$border"} =~ s/^(.*) \#......$/$1 $colour/;
    }else{
        return undef;
    }
}

sub _prepare_float {
    my ($style) = shift;
    my %properties;
    my @sides = split /\./, $style;

    if ($#sides == 1) { # decimal places
        $properties{'number:decimal-places'} = length $sides[1];
    } else {
        $properties{'number:decimal-places'} = 0;
    }
    $properties{'number:min-integer-digits'} = length($sides[0] =~ /0+$/);
    $properties{'number:grouping'} = 'true' if $sides[0] =~ /.,...$/;

    return \%properties;
}

sub _prepare_fraction {
    my ($style) = shift;
    my %properties;
    my @sides = split /[ \/]/, $style;

    $properties{'number:min-integer-digits'} = length($sides[0] =~ /0+$/);
    $properties{'number:min-numerator-digits'} = length($sides[1]);
    $properties{'number:min-denominator-digits'} = length($sides[2]);

    return \%properties;
}

sub _create_positive_style {
    my ($name, $type, $base) = @_;
    my $pstyle = $ods->createStyle(
        $name,
        namespace => 'number',
        type => $type,
        properties => $base,
        references => {
            'style:volatile' => 'true',
            },
        );
    return $pstyle->insert_new_elt('last-child',
        'number:text', {}, ' ');
}

sub _format_handler {
    my ($t, $format) = @_;
    my $style = sprintf 'ce%d', (scalar (keys %style_table) + 1);
    my @extras;

    # SC: There are multiple types of properties that can be associated
    #     with a style.  However, the OO::OOD style creation code appears
    #     to only allow for a single type to be added to the style at a
    #     time.  As a result, %properties is split into property groupings
    #     to allow for each group to get the correct type.
    my %properties;
    if (@style_stack) {
        %properties = %{$style_stack[0][1]};
        if ($celltype{$style_stack[0][0]}) {
            $celltype{$style} = $celltype{$style_stack[0][0]};
            @extras = ('references', {
                'style:data-style-name' => $celltype{$style}[1]
                });
        }
    }
    &_border_set(\%properties, $format, 'border') if $format->{att}->{border};
    while (my ($attr, $val) = each %{$format->{att}}) {
        if ($attr eq 'bottom') {
            &_border_set($format, \%properties, 'border-bottom');
        } elsif ($attr eq 'top') {
            &_border_set($format, \%properties, 'border-top');
        } elsif ($attr eq 'left') {
            &_border_set($format, \%properties, 'border-left');
        } elsif ($attr eq 'right') {
            &_border_set($format, \%properties, 'border-right');
        } elsif ($attr eq 'bg_color' or $attr eq 'bg_colour') {
            if ($val =~ /^\d+$/) {
                $properties{cell}{'fo:background-color'} =
                    $colour[$val - 8];
            } elsif ($val =~ /^\#[0-9A-Fa-f]{6}$/) {
                $properties{cell}{'fo:background-color'} = $val;
            } else {
                $properties{cell}{'fo:background-color'} =
                    $colour_name{$val};
            }
        } elsif ($attr eq 'color' or $attr eq 'colour') {
            if ($val =~ /^\d+$/) {
                $properties{text}{'fo:color'} =
                    $colour[$val - 8];
            } elsif ($val =~ /^\#[0-9A-Fa-f]{6}$/) {
                $properties{text}{'fo:color'} = $val;
            } else {
                $properties{text}{'fo:color'} =
                    $colour_name{$val};
            }
        } elsif ($attr eq 'align') {
            if (lc $val eq 'right') {
                $properties{paragraph}{'fo:text-align'} = 'end';
            } elsif (lc $val eq 'left') {
                $properties{paragraph}{'fo:text-align'} = 'start';
            } else {
                $properties{paragraph}{'fo:text-align'} = $val;
            }
        } elsif ($attr eq 'valign') {
            # takes top, vcenter, bottom, or vjustify
            # needs top, middle, or bottom
            if ($val =~ /^v/i) {
                $properties{paragraph}{'style:vertical-align'} = 'middle';
            } else {
                $properties{paragraph}{'style:vertical-align'} = $val;
            }
        } elsif ($attr eq 'hidden') {
            if ($properties{cell}{'style:cell-protect'} and not $val) {
                delete $properties{cell}{'style:cell-protect'};
            } elsif ($val) {
                $properties{cell}{'style:cell-protect'} = 'formula-hidden';
            }
        } elsif ($attr eq 'font') {
            $properties{text}{'style:font-name'} = $val;
        } elsif ($attr eq 'size') {
            $properties{text}{'fo:font-size'} = "${val}pt";
        } elsif ($attr eq 'bold') {
            if ($properties{text}{'fo:font-weight'} and not $val) {
                delete $properties{text}{'fo:font-weight'};
            } elsif ($val) {
                $properties{text}{'fo:font-weight'} = 'bold';
            }
        } elsif ($attr eq 'italic') {
            if ($properties{text}{'fo:font-style'} and not $val) {
                delete $properties{text}{'fo:font-style'};
            } elsif ($val) {
                $properties{text}{'fo:font-style'} = 'italic';
            }
        } elsif ($attr eq 'font_strikeout') {
            if (!$val) {
                $properties{text}{'style:text-line-through-type'} = 'none';
            } elsif ($val) {
                $properties{text}{'style:text-line-through-type'} = 'single';
            }
        } elsif ($attr eq 'font_shadow') {
            if ($properties{text}{'fo:text-shadow'} and not $val) {
                delete $properties{text}{'fo:text-shadow'};
            } elsif ($val) {
                $properties{text}{'fo:text-shadow'} = '2pt';
            }
        } elsif ($attr eq 'font_outline') {
            if ($properties{text}{'style:text-outline'} and not $val) {
                delete $properties{text}{'style:text-outline'};
            } elsif ($val) {
                $properties{text}{'style:text-outline'} = 'true';
            }
        } elsif ($attr eq 'shrink') {
            if ($properties{cell}{'style:shrink-to-fit'} and not $val) {
                delete $properties{cell}{'style:shrink-to-fit'};
            } elsif ($val) {
                $properties{cell}{'style:shrink-to-fit'} = 'true';
            }
        } elsif ($attr eq 'text_wrap') {
            if (!$val) {
                $properties{cell}{'style:wrap-option'} = 'no-wrap';
            } else {
                $properties{text}{'style:wrap-option'} = 'wrap';
            }
        } elsif ($attr eq 'text_justlast') {
            if ($properties{paragraph}{'fo:text-align-last'} and not $val) {
                delete $properties{paragraph}{'fo:text-align-last'};
            } elsif ($val) {
                $properties{paragraph}{'fo:text-align-last'} = 'justify';
            }
        } elsif ($attr eq 'num_format') {
            #SC: Number formatting is when I hit the point of,
            #    "Screw the OO::OOD API, XML::Twig is simpler".
            #    @children's elements are passed right into the
            #    style via XML::Twig::Elt's insert_new_elt.  The
            #    OO:OOD API, while decent enough for the text
            #    styles, is not so pleasant with complex number
            #    styles.
            my @children;
            my %nproperties;
            my @nextras;
            my $nstyle;
            my $fval = sprintf 'N%02d', $val;
            @extras = ('references', {'style:data-style-name' => $fval});
            if ($style_table{$fval}) {
                # pass through
            } elsif ($val == 0) {
                $celltype{$style} = 'float';
            } elsif ($val == 1) {
                $celltype{$style} = ['float', 'N01'];
                $nstyle = 'number-style';
                %nproperties = %{&_prepare_float('0')}
            } elsif ($val == 2) {
                $celltype{$style} = ['float', 'N02'];
                $nstyle = 'number-style';
                %nproperties = %{&_prepare_float('0.00')}
            } elsif ($val == 3) {
                $celltype{$style} = ['float', 'N03'];
                $nstyle = 'number-style';
                %nproperties = %{&_prepare_float('#,##0')}
            } elsif ($val == 4) {
                $celltype{$style} = ['float', 'N04'];
                $nstyle = 'number-style';
                %nproperties = %{&_prepare_float('#,##0.00')}
            } elsif ($val == 5) { ## ($#,##0_);($#,##0)
            } elsif ($val == 6) {
                $celltype{$style} = 'currency';
            } elsif ($val == 7) {
                $celltype{$style} = 'currency';
            } elsif ($val == 8) {
                $celltype{$style} = 'currency';
            } elsif ($val == 9) { ##      0%
                $celltype{$style} = ['percentage', 'N09'];
                $nstyle = 'percentage-style';
                %nproperties = %{&_prepare_float('0')};
                push @children, ['number:text', {}, '%'];
            } elsif ($val == 10) { ##    0.00%
                $celltype{$style} = ['percentage', 'N10'];
                $nstyle = 'percentage-style';
                %nproperties = %{&_prepare_float('0.00')};
                push @children, ['number:text', {}, '%'];
            } elsif ($val == 11) { ##  0.00E+00
                $celltype{$style} = ['float', 'N11'];
                $nstyle = 'number-style';
                push @children, ['number:scientific-number', {
                        %{&_prepare_float('0.00')},
                        'number:min-exponent-digits' => 2
                        }];
            } elsif ($val == 12) { ## # ?/?
                $celltype{$style} = ['float', 'N12'];
                $nstyle = 'number-style';
                push @children, ['number:fraction',
                    %{&_prepare_fraction('# ?/?')}];
            } elsif ($val == 13) { ## # ??/??
                $celltype{$style} = ['float', 'N13'];
                $nstyle = 'number-style';
                push @children, ['number:fraction',
                    %{&_prepare_fraction('# ??/??')}];
            } elsif ($val == 14) { ##  m/d/yy
                $celltype{$style} = ['date', 'N14'];
                $nstyle = 'date-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true'});
                @children = (
                    ['number:month'],
                    ['number:text', {}, '/'],
                    ['number:day'],
                    ['number:text', {}, '/'],
                    ['number:year'],
                    );
            } elsif ($val == 15) { ## d-mmm-yy
                $celltype{$style} = ['date', 'N15'];
                $nstyle = 'date-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true'});
                @children = (
                    ['number:day'],
                    ['number:text', {}, '-'],
                    ['number:month', {
                        'number:textual' => 'true'}],
                    ['number:text', {}, '-'],
                    ['number:year'],
                    );
            } elsif ($val == 16) { ## d-mmm
                $celltype{$style} = ['date', 'N16'];
                $nstyle = 'date-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true'});
                @children = (
                    ['number:day'],
                    ['number:text', {}, '-'],
                    ['number:month', {
                        'number:textual' => 'true'}],
                    );
            } elsif ($val == 17) { ## mmm-yy
                $celltype{$style} = ['date', 'N17'];
                $nstyle = 'date-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true'});
                @children = (
                    ['number:month', {
                        'number:textual' => 'true'}],
                    ['number:text', {}, '-'],
                    ['number:year'],
                    );
            } elsif ($val == 18) { ## h:mm AM/PM
                $celltype{$style} = ['time', 'N18'];
                $nstyle = 'time-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true'});
                @children = (
                    ['number:hours'],
                    ['number:text', {}, ':'],
                    ['number:minutes',
                        {'number:style' => 'long'}],
                    ['number:text', {}, ' '],
                    ['number:am-pm']
                    );
            } elsif ($val == 19) { ## h:mm:ss AM/PM
                $celltype{$style} = ['time', 'N19'];
                $nstyle = 'time-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true'});
                @children = (
                    ['number:hours'],
                    ['number:text', {}, ':'],
                    ['number:minutes',
                        {'number:style' => 'long'}],
                    ['number:text', {}, ':'],
                    ['number:seconds',
                        {'number:style' => 'long'}],
                    ['number:text', {}, ' '],
                    ['number:am-pm']
                    );
            } elsif ($val == 20) { ## h:mm
                $celltype{$style} = ['time', 'N20'];
                $nstyle = 'time-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true'});
                @children = (
                    ['number:hours'],
                    ['number:text', {}, ':'],
                    ['number:minutes',
                        {'number:style' => 'long'}],
                    );
            } elsif ($val == 21) { ## h:mm:ss
                $celltype{$style} = ['time', 'N21'];
                $nstyle = 'time-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true'});
                @children = (
                    ['number:hours'],
                    ['number:text', {}, ':'],
                    ['number:minutes',
                        {'number:style' => 'long'}],
                    ['number:text', {}, ':'],
                    ['number:seconds',
                        {'number:style' => 'long'}],
                    );
            } elsif ($val == 22) { ## m/d/yy h:mm
                $celltype{$style} = ['date', 'N22'];
                $nstyle = 'date-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true'});
                @children = (
                    ['number:month'],
                    ['number:text', {}, '/'],
                    ['number:day'],
                    ['number:text', {}, '/'],
                    ['number:year'],
                    ['number:text', {}, ' '],
                    ['number:hours'],
                    ['number:text', {}, ':'],
                    ['number:minutes',
                        {'number:style' => 'long'}],
                    );
            } elsif ($val == 37) { ##  (#,##0_);(#,##0)
                $celltype{$style} = ['float', 'N37'];
                $nstyle = 'number-style';
                my %base = (
                    'number:min-integer-digits' => 1,
                    'number:grouping' => 'true',
                    );

                @children = (
                    ['number:text', {}, '('],
                    ['number:number', \%base],
                    ['number:text', {}, ')'],
                    ['style:map', {
                        'style:condition' => 'value()>=0',
                        'style:apply-style-name' => 'NP37',
                        }],
                    );

                &_create_positive_style('NP37',
                    $nstyle, \%base);
            } elsif ($val == 38) { ## (#,##0_);[Red](#,##0)
                $celltype{$style} = ['float', 'N38'];
                $nstyle = 'number-style';
                my %base = %{&_prepare_float('#,##0')};

                @children = (
                    ['style:text-properties',
                        {'fo:color' => '#ff0000'}],
                    ['number:text', {}, '('],
                    ['number:number', \%base],
                    ['number:text', {}, ')'],
                    ['style:map',{
                        'style:condition' => 'value()>=0',
                        'style:apply-style-name' => 'NP38',
                        }]
                    );

                &_create_positive_style('NP38',
                    $nstyle, \%base);
            } elsif ($val == 39) { ## (#,##0.00_);(#,##0.00)
                $celltype{$style} = ['float', 'N39'];
                $nstyle = 'number-style';
                my %base = %{&_prepare_float('#,##0.00')};

                @children = (
                    ['number:text', {}, '('],
                    ['number:number', \%base],
                    ['number:text', {}, ')'],
                    ['style:map',{
                        'style:condition' => 'value()>=0',
                        'style:apply-style-name' => 'NP39',
                        }]
                    );

                &_create_positive_style('NP39',
                    $nstyle, \%base);
            } elsif ($val == 40) { ## (#,##0.00_);[Red](#,##0.00)
                $celltype{$style} = ['float', 'N40'];
                $nstyle = 'number-style';
                my %base = %{&_prepare_float('#,##0.00')};

                @children = (
                    ['style:text-properties',
                        {'fo:color' => '#ff0000'}],
                    ['number:text', {}, '('],
                    ['number:number', \%base],
                    ['number:text', {}, ')'],
                    ['style:map', {
                        'style:condition' => 'value()>=0',
                        'style:apply-style-name' => 'NP40',
                        }],
                    );

                &_create_positive_style('NP40',
                    $nstyle, \%base);
            } elsif ($val == 41) {
                $celltype{$style} = 'float';
            } elsif ($val == 42) {
                $celltype{$style} = 'currency';
            } elsif ($val == 43) {
                $celltype{$style} = 'float';
            } elsif ($val == 44) {
                $celltype{$style} = 'currency';
            } elsif ($val == 45) {  ## mm:ss
                $celltype{$style} = ['time', 'N45'];
                $nstyle = 'time-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true'});
                @children = (
                    ['number:minutes',
                        {'number:style' => 'long'}],
                    ['number:text', {}, ':'],
                    ['number:seconds',
                        {'number:style' => 'long'}],
                    );
            } elsif ($val == 46) { ## [h]:mm:ss
                $celltype{$style} = ['time', 'N46'];
                $nstyle = 'time-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true',
                    'number:truncate-on-overflow' => 'false'});
                @children = (
                    ['number:hours', {}],
                    ['number:text', {}, ':'],
                    ['number:minutes',
                        {'number:style' => 'long'}],
                    ['number:text', {}, ':'],
                    ['number:seconds',
                        {'number:style' => 'long'}],
                    );
            } elsif ($val == 47) { ## mm:ss.0
                $celltype{$style} = ['time', 'N47'];
                $nstyle = 'time-style';
                @nextras = ('references' => {
                    'number:automatic-order' => 'true'});
                @children = (
                    ['number:minutes',
                        {'number:style' => 'long'}],
                    ['number:text', {}, ':'],
                    ['number:seconds',
                        {'number:style' => 'long',
                        'number:decimal-places' => 1}],
                    );
            } elsif ($val == 48) { ##   ##0.0E+0
                $celltype{$style} = ['float', 'N48'];
                $nstyle = 'number-style';
                %nproperties = ();
                push @children, ['number:scientific-number',
                    {%{&_prepare_float(0.0)},
                    'number:min-exponent-digits' => 1
                    }];
            } elsif ($val == 49) {
                $celltype{$style} = 'string';
            }
            # $nstyle is set on new styles
            if ($nstyle) {
                my $cstyle = $ods->createStyle(
                    $celltype{$style}[1],
                    namespace => 'number',
                    type => $nstyle,
                    properties => \%nproperties,
                    @nextras,
                    );
                for my $child (@children) {
                    $cstyle->insert_new_elt('last_child',
                        @$child);
                }
                $style_table{$fval} = 1;
            }
        }
    }

    # Maintain a hash table to keep the final style list size down
    $Data::Dumper::Sortkeys = 1;
    my $mystyle = Digest::MD5::md5_hex(Dumper(\%properties, \@extras));
    if (!$style_table{$mystyle}) {
        $ods->createStyle(
            $style,
            family => 'table-cell',
            properties => $properties{cell},
            @extras,
            );
        $ods->updateStyle(
            $style,
            properties => {
                -area => 'text',
                %{$properties{text}}
                }
            ) if $properties{text};
        $ods->updateStyle(
            $style,
            properties => {
                -area => 'paragraph',
                %{$properties{paragraph}}
                }
            ) if $properties{paragraph};
        $style_table{$mystyle} = [$style, \%properties];
    }
    return unshift @style_stack, $style_table{$mystyle};
}

sub _named_format {
    my ($name, $t, $format) = @_;
    $format->{att}{$name} = 1;
    return &_format_handler($t, $format);
}

sub _format_cleanup_handler {
    my ($t, $format) = @_;
    return shift @style_stack;
}


# _ods_process($output, $template)
#
# If $output is a scalar, it is interpreted as a filename into which
# the rendered template will be written.
#
# If $output is a scalar reference, the rendered template will be written
# into the referenced scalar and no file will be rendered to disk.

sub _ods_process {
    my ($output, $template) = @_;
    my $workdir = File::Temp->newdir;
    my $fn;
    my $fh; # if we need to use a temp file, we need to keep the object
            # in scope, because when it goes out of scope, the file is removed
    if (ref $output) {
        $fh = File::Temp->new( DIR => $workdir );
        $fn = $fh->filename;
    }
    else {
        $fn = $output;
    }

    odfWorkingDirectory($workdir->dirname);
    $ods = ooDocument(
        file => $fn,
        create => 'spreadsheet',
    );

    my $parser = XML::Twig->new(
        start_tag_handlers => {
            worksheet => \&_worksheet_handler,
            row => \&_row_handler,
            cell => \&_cell_handler,
            formula => \&_formula_handler,
            format => \&_format_handler,
            bold => sub { &_named_format('bold', @_) },
            hidden => sub { &_named_format('hidden', @_) },
            italic => sub { &_named_format('italic', @_) },
            shadow => sub { &_named_format('shadow', @_) },
            strikeout => sub { &_named_format('strikeout', @_) },
            },
        twig_handlers => {
            format => \&_format_cleanup_handler,
            bold => \&_format_cleanup_handler,
            hidden => \&_format_cleanup_handler,
            italic => \&_format_cleanup_handler,
            shadow => \&_format_cleanup_handler,
            strikeout => \&_format_cleanup_handler,
            }
        );
    $parser->parse($template);
    $parser->purge;
    $ods->save;

    if (ref $output) {
       my $size = (stat($fn))[7];
       open(my $inp, '<', $fn)
         or die "Unable to open temporary file for reading ODS output: $!";
       sysread($inp, $$output, $size)
         or die "Error: $!";
       $fh->close
         or die "Can't clean up ODS generation temporary file: $!";
    }
    return undef;
}

=head1 METHODS

=head2 setup($parent, $cleanvars, $output)

Implements the template's initialization protocol.

=cut

sub setup {
    my ($self, $parent, $cleanvars, $output) = @_;

    my $temp_output;
    return (\$temp_output, {
        input_extension => 'odst',
        binmode => ':utf8',
        _output => $output,
    });
}

=head2 postprocess($parent, $output, $config)

Implements the template's post-processing protocol.

=cut

sub postprocess {
    my ($self, $parent, $output, $config) = @_;

    &_ods_process($config->{_output}, $$output);
    return undef;
}

=head2 mimetype()

Returns the rendered template's mimetype.

=cut

sub mimetype {
    my $self = shift;
    my $config = shift;
    return 'application/vnd.oasis.opendocument.spreadsheet';
}

=head1 LICENSE AND COPYRIGHT

Copyright (C) 2007-2022 The LedgerSMB Core Team

This file is licensed under the GNU General Public License version 2, or at your
option any later version.  A copy of the license should have been included with
your software.

=cut

1;
