# Copyright (c) 2010 Ars Aperta, Itaapy, Pierlis, Talend.
#
# Author: Jean-Marie Gouarné <jean-marie.gouarne@arsaperta.com>
#
# This file is part of lpOD (see: http://lpod-project.org).
# lpOD is free software; you can redistribute it and/or modify it under
# the terms of either:
#
# a) the GNU General Public License as published by the Free Software
#    Foundation, either version 3 of the License, or (at your option)
#    any later version.
#    Lpod is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#    You should have received a copy of the GNU General Public License
#    along with lpOD.  If not, see <http://www.gnu.org/licenses/>.
#
# b) the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
#    http://www.apache.org/licenses/LICENSE-2.0
#-----------------------------------------------------------------------------
use     5.010_000;
use     strict;
#-----------------------------------------------------------------------------
#       Text Element classes
#-----------------------------------------------------------------------------
package ODF::lpOD::TextElement;
use base 'ODF::lpOD::Element';
our $VERSION    = '1.000';
use constant PACKAGE_DATE => '2010-12-24T12:44:28';
use ODF::lpOD::Common;
#=============================================================================

BEGIN   {
        *set_link               = *set_hyperlink
        }

#--- constructor -------------------------------------------------------------

sub     create
        {
        my %opt = process_options
                (
                tag     => undef,
                style   => undef,
                text    => undef,
                @_
                );

        my $tag = $opt{tag}; $tag = 'text:' . $tag unless $tag =~ /:/;
        my $e = odf_element->new($tag) or return undef;
        if ($tag eq 'text:h')
                {
                $e->set_attribute('outline level', $opt{level} // 1);
                $e->set_attribute('restart numbering', 'true')
                                if is_true($opt{'restart_numbering'});
                $e->set_attribute('start value', $opt{start_value})
                                if defined $opt{start_value};
                $e->set_attribute('is list header', 'true')
                                if defined $opt{suppress_numbering};
                }
        $e->set_style($opt{style})
                        if defined $opt{style};
        $e->set_text($opt{text})
                        if defined $opt{text};

        return $e;
        }

#=== common tools ============================================================

sub     set_spaces
        {
        my $self        = shift;
        my $count       = shift         or return undef;
        my %opt         = @_;
        
        my $s = $self->insert_element('s', %opt);
        $s->set_attribute('c', $count);
        return $s;
        }

sub     set_line_break
        {
        my $self        = shift;
        return $self->insert_element('line break', @_);
        }

sub     set_tab_stop
        {
        my $self        = shift;
        return $self->insert_element('tab', @_);
        }

#--- split the content with new child elements -------------------------------

sub     split_content
        {
        my $self        = shift;
        my %opt         =
                (
                tag             => undef,
                search          => undef,
                offset          => undef,
                length          => undef,
                content         => undef,
                insert          => undef,
                attributes      => {},
                @_
                );
        if (defined $opt{search} && defined $opt{length})
                {
                alert "Conflicting search and length parameters";
                return FALSE;
                }
        if (is_true($opt{repeat}))
                {
                delete $opt{repeat};
                my $start = $opt{start_mark};
                if ($opt{offset})
                        {
                        $start = $self->split_content(%opt);
                        }
                $opt{offset} = 0;
                my @elts = ();
                do      {
                        $opt{start_mark} = $start;
                        $start = $self->split_content(%opt);
                        push @elts, $start;
                        }
                while ($start);
                return @elts;
                }
        my $tag         = $self->normalize_name($opt{tag});
        my $search      = $opt{search};
        if (defined $opt{start_mark} || defined $opt{end_mark})
                {
                $opt{offset} //= 0;
                }
        if (defined $search && ! defined $opt{offset})
                {
                my $t = defined $opt{text} ?
                        input_conversion($opt{text}) : undef;
                my $attr =
                        $self->input_convert_attributes($opt{attributes});
                my $expr = input_conversion($search);
                my @elts = $self->mark ("($expr)", $tag, $attr);
                for (@elts)
                        {
                        $_->_set_text($t) if defined $t;
                        $_->set_class;
                        }
                return @elts;
                }
        else
                {
                my $position = $opt{offset} || 0;
                if ($position eq 'end')
                        {
                        my $e = $self->append_element($tag);
                        $e->set_attributes($opt{attributes});
                        $e->set_text($opt{text});
                        $e->set_class;
                        return $e;
                        }
                if ($position == 0 && ! defined $search)
                        {
                        my $e = $self->insert_element($tag);
                        $e->set_attributes($opt{attributes});
                        $e->set_text($opt{text});
                        $e->set_class;
                        return $e;
                        }
                my $range = $opt{length};
                my %r = $self->search
                                (
                                $search,
                                offset          => $position,
                                range           => $range,
                                backward        => $opt{backward},
                                start_mark      => $opt{start_mark},
                                end_mark        => $opt{end_mark}
                                );
                if (defined $r{segment})
                        {
                        my $e = odf_create_element($tag);
                        unless ($opt{insert})
                                {
                                my $t = $r{segment}->_get_text;
                                $range = $r{end} - $r{offset}
                                                if defined $search;
                                if (defined $range)
                                        {
                                        substr($t, $r{offset}, $range, "")
                                        }
                                else
                                        {
                                        $t = substr($t, 0, $r{offset});
                                        }
                                $r{segment}->_set_text($t);
                                $e->set_text($opt{text} // $r{match});
                                if      (
                                                (
                                                defined $opt{offset}
                                                        &&
                                                $opt{offset} >= 0
                                                )
                                                ||
                                                defined $search
                                        )
                                        {
                                        $e->paste_within
                                                ($r{segment}, $r{offset});
                                        }
                                else
                                        {
                                        if ($r{end} < 0)
                                                {
                                                $e->paste_within
                                                        ($r{segment}, $r{end});
                                                }
                                        else
                                                {
                                                $e->paste_after($r{segment});
                                                }
                                        }

                                }
                        else
                                {
                                my $p = $opt{insert} eq 'after' ?
                                        $r{end} : $r{offset};
                                $e->paste_within($r{segment}, $p);
                                $e->set_text($opt{text});
                                }

                        $e->set_attributes($opt{attributes});
                        $e->set_class;
                        return $e;
                        }
                }
        return FALSE;
        }

#--- lpOD-specific bookmark setting ------------------------------------------

sub     set_lpod_mark
        {
        my $self        = shift;
        my %opt         = @_;
        my $id;
        if (defined $opt{id})
                {
                $id = $opt{id}; delete $opt{id};
                }
        else
                {
                $id = lpod_common->new_id;
                }
        $opt{tag}               = $ODF::lpOD::Common::LPOD_MARK;
        $opt{attributes}        =
                {
                $ODF::lpOD::Common::LPOD_ID     => $id
                };
        return $self->split_content(%opt);
        }

#--- common bookmark, index mark setting tool --------------------------------

sub     set_position_mark
        {
        my $self        = shift;
        my $tag         = shift;
        my %opt         =
                (
                offset          => undef,
                before          => undef,
                after           => undef,
                @_
                );
        if (defined $opt{before} && defined $opt{after})
                {
                alert "Conflicting before and after parameters";
                return FALSE;
                }
        
        $opt{offset}  //= 0;
        $opt{search}    = $opt{before} // $opt{after};
        if      (defined $opt{after})   { $opt{insert} = 'after'  }
        else                            { $opt{insert} = 'before' }
        $opt{length}    = defined $opt{search} ? undef : 0;

        delete @opt{qw(before after)};
        $opt{tag} = $tag;
        return $self->split_content(%opt);
        }

sub     set_text_mark
        {
        my $self        = shift;
        my %opt         = @_;
        if (defined $opt{content} || ref $opt{offset})
                {
                my $content = $opt{content};
                my ($p1, $p2, $range_end);
                if (ref $opt{offset})
                        {
                        $p1 = $opt{offset}[0];
                        $p2 = $opt{offset}[1];
                        $range_end = $self->set_lpod_mark
                                        (offset => $p2, length => 0)
                                if defined $p2 && defined $opt{content};
                        $opt{end_mark} = $range_end if $range_end;
                        }
                else
                        {
                        $p1 = $opt{offset};
                        $p2 = $opt{offset};
                        }
                delete @opt{qw(content offset)};
                $opt{offset}  = $p1;
                $opt{before}    = $content      if defined $content;
                $opt{role}      = 'start';
                my $start = $self->set_text_mark(%opt)
                        or return FALSE;
                $opt{offset}  = $p2;
                if (defined $content)
                        {
                        $opt{after}     = $content;
                        delete $opt{before};
                        }
                $opt{role}      = 'end';
                my $end   = $self->set_text_mark(%opt)
                        or return FALSE;
                unless ($start->before($end))
                        {
                        $start->delete; $end->delete;
                        alert("Start is not before end");
                        return FALSE;
                        }
                if ($range_end)
                        {
                        $range_end->delete(); $self->normalize;
                        }
                return wantarray ? $start : ($start, $end);
                }

        my $tag;
        given ($opt{role})
                {
                when (undef)
                        {
                        $tag = $opt{tag};
                        }
                when (/^(start|end)$/)
                        {
                        $tag = $opt{tag} . '-' . $_;
                        delete $opt{role};
                        }
                default
                        {
                        alert("Wrong role = $_ option");
                        return undef;
                        }
                }
        
        delete $opt{tag};
        return $self->set_position_mark($tag, %opt);
        }

#=== text content handling ===================================================

sub     set_text
        {
        my $self        = shift;
        my $text        = shift;
        return $self->SUPER::set_text($text, @_)    unless $text;
        return $self->_set_text($text)  if caller() eq 'XML::Twig::Elt';
        
        $self->_set_text("");
        my @lines = split("\n", $text);
        while (@lines)
                {
                my $line = shift @lines;
                my @columns = split("\t", $line);
                while (@columns)
                        {
                        my $column = shift @columns;
                        my @words = split(/(\s\s+)/, $column);
                        foreach my $word (@words)
                                {
                                my $l = length($word);
                                if ($word =~ m/^ +$/)
                                        {
                                        $self->set_spaces
                                            ($l, position => 'LAST_CHILD');
                                        }
                                elsif ($l > 0)
                                        {
                                        my $n = $self->append_element
                                                        (TEXT_SEGMENT);
                                        $n->set_text($word);
                                        }
                                }
                        $self->append_element('tab') if @columns;
                        }
                $self->append_element('line break') if @lines;
                }
        $self->normalize;
        return TRUE;
        }

sub     get_text
        {
        my $self        = shift;
        my %opt         = @_;
        
        unless (is_true($opt{recursive}))
                {
                return $self->SUPER::get_text;
                }
        
        my $text        = undef;
        foreach my $node ($self->children)
                {
                given ($node->get_tag)
                        {
                        when ('text:tab')
                                {
                                $text .= $ODF::lpOD::Common::TAB_STOP;
                                }
                        when ('text:line-break')
                                {
                                $text .= $ODF::lpOD::Common::LINE_BREAK;
                                }
                        when ('text:s')
                                {
                                my $c = $node->get_attribute('c') // 1; #/
                                $text .= " " while $c-- > 0;
                                }
                        default
                                {
                                my $t = $node->get_text(%opt);
                                $text .= $t if defined $t;
                                }
                        }
                }

        return $text;
        }

#=== text internal markup ===================================================

sub     set_span
        {
        my $self        = shift;
        my %opt         = @_;
        unless ($opt{style})
                {
                alert("Missing style name");
                return FALSE;
                }
        $opt{search} = $opt{filter} if exists $opt{filter};  
        $opt{attributes} = { 'style name' => $opt{style} };
        delete @opt{qw(filter style)};
        return $self->split_content(tag => 'span', %opt);
        }

sub	remove_spans
	{
	my $self	= shift;

        my $tmp = $self->clone;
        $self->delete_children;
        my $count       = 0;
        foreach my $e ($tmp->descendants)
                {
                unless ($e->is('text:span'))
                        {
                        $e->move(last_child => $self);
                        }
                else
                        {
                        $count++;
                        }
                }
        $tmp->delete;
        return $count;
	}

sub     set_hyperlink
        {
        my $self        = shift;
        my %opt         = process_options(@_);
        my $url         = $opt{url};
        delete $opt{url};
        unless ($url)
                {
                alert("Missing URL"); return FALSE;
                }
        $opt{search} = $opt{filter} if exists $opt{filter};
        $opt{attributes} =
                {
                'xlink:href'            => $url,
                'office:name'           => $opt{name},
                'office:title'          => $opt{title},
                'style name'            => $opt{style},
                'visited style name'    => $opt{visited_style}
                };
        delete @opt{qw(filter name title style visited_style)};
        return $self->split_content(tag => 'a', %opt);
        }

sub     set_bookmark
        {
        my $self        = shift;
        my $name        = shift;
        unless ($name)
                {
                alert "Missing bookmark name"; return FALSE;
                }

        return $self->set_text_mark
                (
                tag             => 'bookmark',
                attributes      =>
                        {
                        name            => $name
                        },
                @_
                );
        }

sub     set_index_mark
        {
        my $self        = shift;
        my $text        = shift;
        
        unless ($text)
                {
                alert "Missing index entry text";
                return FALSE;
                }
        
        my %opt         = process_options (@_);

        if ($opt{index_name})
                {
                $opt{type} ||= 'user';
                unless ($opt{type} eq 'user')
                        {
                        alert "Index mark type must be user";
                        return FALSE;
                        }
                }
        else
                {
                $opt{type} ||= 'lexical';
                }
        my $tag;
        my %attr = $opt{attributes} ? %{$opt{attributes}} : ();
        given ($opt{type})
                {
                when (["lexical", "alphabetical"])
                        {
                        $tag = 'alphabetical index mark';
                        }
                when ('toc')
                        {
                        $tag = 'toc mark';
                        $attr{'outline level'} = $opt{level} // 1;
                        }
                when ('user')
                        {
                        unless ($opt{index_name})
                                {
                                alert "Missing index name";
                                return FALSE;
                                }
                        $tag = 'user index mark';
                        $attr{'outline level'} = $opt{level} // 1;
                        }
                default
                        {
                        alert "Wrong index mark type ($opt{type})";
                        return FALSE
                        }
                }

        if (defined $opt{content} || ref $opt{offset} || $opt{role})
                {       # it's a range index mark
                $attr{'id'} = $text;
                }
        else
                {
                $attr{'string value'} = $text;
                }

        delete @opt{qw(type index_name level attributes)};
        $opt{attributes} = {%attr};
        return $self->set_text_mark(tag => $tag, %opt);
        }

sub     set_bibliography_mark
        {
        my $self        = shift;
        my %opt         = process_options(@_);

        my $type_ok;
        foreach my $k (keys %opt)
                {
                if (ref $opt{$k} || ($k ~~ ['content', 'role']))
                        {
                        alert "Not allowed option";
                        delete $opt{$k};
                        next;
                        }
                unless  (
                        $k ~~   [
                                'before', 'after', 'offset',
                                'start_mark', 'end_mark'
                                ]
                        )
                        {
                        if ($k eq 'type')
                                {
                                $type_ok = TRUE;
                                $k = 'bibliography type';
                                }
                        $opt{attributes}{$k} = $opt{$k};
                        delete $opt{$k};
                        }
                }
        alert "Missing type parameter" unless $type_ok;
        
        return $self->set_position_mark('bibliography mark', %opt);
        }

#=== text notes ==============================================================

sub     set_note
        {
        my $self        = shift;
        my $id          = shift;
        unless ($id)
                {
                alert "Missing note identifier"; return FALSE;
                }
        my %opt         = process_options(@_);
        $opt{attributes} =
                {
                'id'            => $id,
                'note class'    => $opt{class}  || $opt{note_class}
                                                || 'footnote'
                };
        my $style       = $opt{style};
        my $text        = $opt{text};
        my $body        = $opt{body};
        my $citation    = $opt{citation};
        my $label       = $opt{label};
        delete @opt{qw(note_class style text body citation label)};

        my $note        = $self->set_position_mark('note', %opt);
        $note->set_citation($citation, $label);
        $note->{style}  = $opt{style};
        if ($body)
                {
                $note->set_body(@{$body});
                }
        else
                {
                $note->set_body($text);
                }

        return $note;
        }

sub     set_annotation
        {
        my $self        = shift;
        my %opt         = process_options(@_);
        my $date        = $opt{date};
        my $author      = $opt{author};
        my $style       = $opt{style};
        my $content     = $opt{content};
        delete @opt{qw(date author style content)};
        my $a = $self->set_position_mark('office:annotation', %opt);
        $a->set_date($date);
        $a->set_author($author);
        $a->set_style($style);
        $a->set_content(@{$content})    if $content;
        return $a;
        }

#=== text fields =============================================================

sub	set_field
	{
	my $self	= shift;
        my $type        = shift;
        unless ($type)
                {
                alert "Missing field type"; return undef;
                }
	my %opt         = process_options(@_);
        $type = 'user field get' if $type eq 'variable';
        if ($type =~ /^user field/)
                {
                unless ($opt{name})
                        {
                        alert "Missing associated variable name";
                        return undef;
                        }
                }
        else
                {
                unless (ODF::lpOD::TextField::check_type($type))
                        {
                        alert "Unsupported field type"; return undef;
                        }
                }
        OPTION: foreach my $k (keys %opt)
                {
                if (ref $opt{$k} || ($k eq 'role'))
                        {
                        delete $opt{$k};
                        next;
                        }
                unless  (
                        $k ~~   [
                                'before', 'after', 'offset', 'length',
                                'start_mark', 'end_mark', 'search'
                                ]
                        )
                        {
                        given ($k)
                                {
                                when ('fixed')
                                        {
                                        $opt{attributes}{$k} =
                                                odf_boolean($opt{$k});
                                        }
                                when ('style')
                                        {
                                        my $a = 'style:data-style-name';
                                        $opt{attributes}{$a} = $opt{$k};
                                        }
                                default
                                        {
                                        $opt{attributes}{$k} = $opt{$k};
                                        }
                                }
                        delete $opt{$k};
                        }
                }
        my $tag = 'text:' . $type;
        my $field;
        if (defined $opt{search} || defined $opt{length})
                {
                $opt{text} = '';
                $field = $self->split_content(tag => $tag, %opt);
                }
        else
                {
                $field = $self->set_position_mark($tag , %opt);
                }
        return $field;
	}

#=============================================================================
package ODF::lpOD::Paragraph;
use base 'ODF::lpOD::TextElement';
our $VERSION    = '1.000';
use constant PACKAGE_DATE => '2010-12-24T12:52:27';
use ODF::lpOD::Common;
#--- constructor -------------------------------------------------------------

sub     create
        {
        return ODF::lpOD::TextElement::create(tag => 'p', @_);
        }

#=============================================================================
package ODF::lpOD::Heading;
use base 'ODF::lpOD::Paragraph';
our $VERSION    = '1.000';
use constant PACKAGE_DATE => '2010-12-24T12:52:48';
use ODF::lpOD::Common;
#--- constructor -------------------------------------------------------------

sub     create
        {
        return ODF::lpOD::TextElement::create(tag => 'h', @_);
        }

#--- attribute accessors -----------------------------------------------------

sub     get_level
        {
        my $self        = shift;
        return $self->get_attribute('outline level');
        }

sub     set_level
        {
        my $self        = shift;
        return $self->set_attribute('outline level', @_);
        }

sub     get_suppress_numbering
        {
        my $self        = shift;
        return $self->get_boolean_attribute('is list header');
        }

sub     set_suppress_numbering
        {
        my $self        = shift;
        return $self->set_boolean_attribute('is list header', shift);
        }

sub     set_start_value
        {
        my $self        = shift;
        my $number      = shift;
        unless ($number >= 0)
                {
                alert('Wrong start value');
                return FALSE;
                }
        $self->set_attribute('restart numbering', TRUE);
        $self->set_attribute('start value', $number);
        }

#=============================================================================
1;
