package org.apache.maven.doxia.module.itext;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under 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
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import com.lowagie.text.BadElementException;
import com.lowagie.text.ElementTags;
import com.lowagie.text.Image;

import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;

import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.SinkEventAttributes;
import org.apache.maven.doxia.sink.impl.AbstractXmlSink;
import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
import org.apache.maven.doxia.util.DoxiaUtils;
import org.apache.maven.doxia.util.HtmlTools;

import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
import org.codehaus.plexus.util.xml.XMLWriter;

/**
 * <p>A doxia Sink which produces an XML Front End document for <code>iText</code> framework.</p>
 * Known limitations:
 * <ul>
 * <li>Roman lists are not supported.</li>
 * <li>Horizontal rule is not supported with 1.3.
 * See <a href="http://www.mail-archive.com/itext-questions@lists.sourceforge.net/msg10323.html">
 * http://www.mail-archive.com/itext-questions@lists.sourceforge.net/msg10323.html</a></li>
 * <li>iText has some problems with <code>ElementTags.TABLE</code> and <code>ElementTags.TABLEFITSPAGE</code>.
 * See <a href="http://sourceforge.net/tracker/index.php?func=detail&aid=786427&group_id=15255&atid=115255">
 * SourceForce Tracker</a>.</li>
 * <li>Images could be on another page and next text on the last one.</li>
 * </ul>
 *
 * @see <a href="http://www.lowagie.com/iText/tutorial/ch07.html">http://www.lowagie.com/iText/tutorial/ch07.html</a>
 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
 */
public class ITextSink
    extends AbstractXmlSink
{
    /** This is the place where the iText DTD is located. IMPORTANT: this DTD is not uptodate! */
    public static final String DTD = "http://itext.sourceforge.net/itext.dtd";

    /** This is the reference to the DTD. */
    public static final String DOCTYPE = "ITEXT SYSTEM \"" + DTD + "\"";

    /** This is the default leading for chapter title */
    public static final String DEFAULT_CHAPTER_TITLE_LEADING = "36.0";

    /** This is the default leading for section title */
    public static final String DEFAULT_SECTION_TITLE_LEADING = "24.0";

    /** The ClassLoader used */
    private ClassLoader currentClassLoader;

    /** The action context */
    private SinkActionContext actionContext;

    /** The Writer used */
    private Writer writer;

    /** The XML Writer used */
    private final XMLWriter xmlWriter;

    private boolean writeStart;

    /** The Header object */
    private ITextHeader header;

    /** The font object */
    private ITextFont font;

    private int numberDepth = 1;

    private int depth = 0;

    private StringWriter tableCaptionWriter = null;

    private XMLWriter tableCaptionXMLWriter = null;

    /** Flag to know if an anchor is defined or not. Used as workaround for iText which needs a defined local
     * destination. */
    private boolean anchorDefined = false;

    /** Flag to know if an figure event is called. */
    private boolean figureDefined = false;

    /** Keep track of the closing tags for inline events. */
    protected Stack<List<String>> inlineStack = new Stack<>();

    /** Map of warn messages with a String as key to describe the error type and a Set as value.
     * Using to reduce warn messages. */
    private Map<String, Set<String>> warnMessages;

    /**
     * <p>Constructor for ITextSink.</p>
     *
     * @param writer the writer.
     */
    protected ITextSink( Writer writer )
    {
        this( writer, "UTF-8" );
    }

    /**
     * <p>Constructor for ITextSink.</p>
     *
     * @param writer the writer.
     * @param encoding the encoding.
     * @since 1.1
     */
    protected ITextSink( Writer writer, String encoding )
    {
        // No doctype since itext doctype is not up to date!
        this( new PrettyPrintXMLWriter( writer, encoding, null ) );

        this.writer = writer;
        this.writeStart = true;
    }

    /**
     * <p>Constructor for ITextSink.</p>
     *
     * @param xmlWriter a pretty-printing xml writer.
     */
    protected ITextSink( PrettyPrintXMLWriter xmlWriter )
    {
        this.xmlWriter = xmlWriter;

        this.writeStart = false;

        init();
    }

    /**
     * Get the current classLoader
     *
     * @return the current class loader
     */
    public ClassLoader getClassLoader()
    {
        return currentClassLoader;
    }

    /**
     * Set a new class loader
     *
     * @param cl the class loader.
     */
    public void setClassLoader( ClassLoader cl )
    {
        currentClassLoader = cl;
    }

    // ----------------------------------------------------------------------
    // Document
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void close()
    {
        IOUtil.close( writer );

        init();
    }

    /**
     * {@inheritDoc}
     */
    public void flush()
    {
        if ( getLog().isWarnEnabled() && this.warnMessages != null )
        {
            for ( Map.Entry<String, Set<String>> entry : this.warnMessages.entrySet() )
            {
                for ( String msg : entry.getValue() )
                {
                    getLog().warn( msg );
                }
            }
        }

        this.warnMessages = null;
    }

    // ----------------------------------------------------------------------
    // Header
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void head_()
    {
        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void head()
    {
        //init(); // why? this causes DOXIA-413

        actionContext.setAction( SinkActionContext.HEAD );
    }

    /**
     * {@inheritDoc}
     */
    public void author_()
    {
        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void author()
    {
        actionContext.setAction( SinkActionContext.AUTHOR );
    }

    /**
     * {@inheritDoc}
     */
    public void date_()
    {
        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void date()
    {
        actionContext.setAction( SinkActionContext.DATE );
    }

    /**
     * {@inheritDoc}
     */
    public void title_()
    {
        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void title()
    {
        actionContext.setAction( SinkActionContext.TITLE );
    }

    // ----------------------------------------------------------------------
    // Body
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void body_()
    {
        if ( writeStart )
        {
            writeEndElement(); // ElementTags.CHAPTER

            writeEndElement(); // ElementTags.ITEXT
        }

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void body()
    {
        if ( writeStart )
        {
            writeStartElement( ElementTags.ITEXT );
            writeAddAttribute( ElementTags.TITLE, header.getTitle() );
            writeAddAttribute( ElementTags.AUTHOR, header.getAuthors() );
            writeAddAttribute( ElementTags.CREATIONDATE, header.getDate() );
            writeAddAttribute( ElementTags.SUBJECT, header.getTitle() );
            writeAddAttribute( ElementTags.KEYWORDS, "" );
            writeAddAttribute( ElementTags.PRODUCER, "Generated with Doxia by " + System.getProperty( "user.name" ) );
            writeAddAttribute( ElementTags.PAGE_SIZE, ITextUtil.getPageSize( ITextUtil.getDefaultPageSize() ) );

            writeStartElement( ElementTags.CHAPTER );
            writeAddAttribute( ElementTags.NUMBERDEPTH, numberDepth );
            writeAddAttribute( ElementTags.DEPTH, depth );
            writeAddAttribute( ElementTags.INDENT, "0.0" );

            writeStartElement( ElementTags.TITLE );
            writeAddAttribute( ElementTags.LEADING, DEFAULT_CHAPTER_TITLE_LEADING );
            writeAddAttribute( ElementTags.FONT, ITextFont.DEFAULT_FONT_NAME );
            writeAddAttribute( ElementTags.SIZE, ITextFont.getSectionFontSize( 0 ) );
            writeAddAttribute( ElementTags.STYLE, ITextFont.BOLD );
            writeAddAttribute( ElementTags.BLUE, ITextFont.DEFAULT_FONT_COLOR_BLUE );
            writeAddAttribute( ElementTags.GREEN, ITextFont.DEFAULT_FONT_COLOR_GREEN );
            writeAddAttribute( ElementTags.RED, ITextFont.DEFAULT_FONT_COLOR_RED );
            writeAddAttribute( ElementTags.ALIGN, ElementTags.ALIGN_CENTER );

//            startChunk( ITextFont.DEFAULT_FONT_NAME, ITextFont.getSectionFontSize( 0 ),
//                    ITextFont.BOLD, ITextFont.DEFAULT_FONT_COLOR_BLUE, ITextFont.DEFAULT_FONT_COLOR_GREEN,
//                    ITextFont.DEFAULT_FONT_COLOR_RED, "top" );

            writeStartElement( ElementTags.CHUNK );
            writeAddAttribute( ElementTags.FONT, ITextFont.DEFAULT_FONT_NAME );
            writeAddAttribute( ElementTags.SIZE, ITextFont.getSectionFontSize( 0 ) );
            writeAddAttribute( ElementTags.STYLE, ITextFont.BOLD );
            writeAddAttribute( ElementTags.BLUE, ITextFont.DEFAULT_FONT_COLOR_BLUE );
            writeAddAttribute( ElementTags.GREEN, ITextFont.DEFAULT_FONT_COLOR_GREEN );
            writeAddAttribute( ElementTags.RED, ITextFont.DEFAULT_FONT_COLOR_RED );
//            writeAddAttribute( ElementTags.LOCALDESTINATION, "top" );

            write( header.getTitle() );

            writeEndElement(); // ElementTags.CHUNK

            writeEndElement(); // ElementTags.TITLE
        }

        actionContext.setAction( SinkActionContext.BODY );
    }

    // ----------------------------------------------------------------------
    // Sections
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void sectionTitle()
    {
        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void sectionTitle_()
    {
        actionContext.setAction( SinkActionContext.SECTION_TITLE );
    }

    /**
     * {@inheritDoc}
     */
    public void section1_()
    {
        writeEndElement(); // ElementTags.SECTION

        numberDepth--;
        depth = 0;

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void section1()
    {
        numberDepth++;
        depth = 1;

        writeStartElement( ElementTags.SECTION );
        writeAddAttribute( ElementTags.NUMBERDEPTH, numberDepth );
        writeAddAttribute( ElementTags.DEPTH, depth );
        writeAddAttribute( ElementTags.INDENT, "0.0" );

        lineBreak();

        actionContext.setAction( SinkActionContext.SECTION_1 );
    }

    /**
     * {@inheritDoc}
     */
    public void sectionTitle1_()
    {
        writeEndElement(); // ElementTags.TITLE

        font.setSize( ITextFont.DEFAULT_FONT_SIZE );
        bold_();

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void sectionTitle1()
    {
        font.setSize( ITextFont.getSectionFontSize( 1 ) );
        font.setColor( Color.BLACK );
        bold();

        writeStartElement( ElementTags.TITLE );
        writeAddAttribute( ElementTags.LEADING, DEFAULT_SECTION_TITLE_LEADING );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );
//        writeAddAttribute( ElementTags.LOCALDESTINATION, "top" ); // trygve

        actionContext.setAction( SinkActionContext.SECTION_TITLE_1 );
    }

    /**
     * {@inheritDoc}
     */
    public void section2_()
    {
        writeEndElement(); // ElementTags.SECTION

        numberDepth--;
        depth = 0;

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void section2()
    {
        numberDepth++;
        depth = 1;

        writeStartElement( ElementTags.SECTION );
        writeAddAttribute( ElementTags.NUMBERDEPTH, numberDepth );
        writeAddAttribute( ElementTags.DEPTH, depth );
        writeAddAttribute( ElementTags.INDENT, "0.0" );

        lineBreak();

        actionContext.setAction( SinkActionContext.SECTION_2 );
    }

    /**
     * {@inheritDoc}
     */
    public void sectionTitle2_()
    {
        writeEndElement(); // ElementTags.TITLE

        font.setSize( ITextFont.DEFAULT_FONT_SIZE );
        bold_();

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void sectionTitle2()
    {
        font.setSize( ITextFont.getSectionFontSize( 2 ) );
        font.setColor( Color.BLACK );
        bold();

        writeStartElement( ElementTags.TITLE );
        writeAddAttribute( ElementTags.LEADING, DEFAULT_SECTION_TITLE_LEADING );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );
//        writeAddAttribute( ElementTags.LOCALDESTINATION, "top" ); // trygve

        actionContext.setAction( SinkActionContext.SECTION_TITLE_2 );
    }

    /**
     * {@inheritDoc}
     */
    public void section3_()
    {
        writeEndElement(); // ElementTags.SECTION

        numberDepth--;
        depth = 1;

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void section3()
    {
        numberDepth++;
        depth = 1;

        writeStartElement( ElementTags.SECTION );
        writeAddAttribute( ElementTags.NUMBERDEPTH, numberDepth );
        writeAddAttribute( ElementTags.DEPTH, depth );
        writeAddAttribute( ElementTags.INDENT, "0.0" );

        lineBreak();

        actionContext.setAction( SinkActionContext.SECTION_3 );
    }

    /**
     * {@inheritDoc}
     */
    public void sectionTitle3_()
    {
        writeEndElement(); // ElementTags.TITLE

        font.setSize( ITextFont.DEFAULT_FONT_SIZE );
        bold_();

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void sectionTitle3()
    {
        font.setSize( ITextFont.getSectionFontSize( 3 ) );
        font.setColor( Color.BLACK );
        bold();

        writeStartElement( ElementTags.TITLE );
        writeAddAttribute( ElementTags.LEADING, DEFAULT_SECTION_TITLE_LEADING );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );
//        writeAddAttribute( ElementTags.LOCALDESTINATION, "top" ); // trygve

        actionContext.setAction( SinkActionContext.SECTION_TITLE_3 );
    }

    /**
     * {@inheritDoc}
     */
    public void section4_()
    {
        writeEndElement(); // ElementTags.SECTION

        numberDepth--;
        depth = 1;

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void section4()
    {
        numberDepth++;
        depth = 1;

        writeStartElement( ElementTags.SECTION );
        writeAddAttribute( ElementTags.NUMBERDEPTH, numberDepth );
        writeAddAttribute( ElementTags.DEPTH, depth );
        writeAddAttribute( ElementTags.INDENT, "0.0" );

        lineBreak();

        actionContext.setAction( SinkActionContext.SECTION_4 );
    }

    /**
     * {@inheritDoc}
     */
    public void sectionTitle4_()
    {
        writeEndElement(); // ElementTags.TITLE

        font.setSize( ITextFont.DEFAULT_FONT_SIZE );
        bold_();

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void sectionTitle4()
    {
        font.setSize( ITextFont.getSectionFontSize( 4 ) );
        font.setColor( Color.BLACK );
        bold();

        writeStartElement( ElementTags.TITLE );
        writeAddAttribute( ElementTags.LEADING, DEFAULT_SECTION_TITLE_LEADING );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );
//        writeAddAttribute( ElementTags.LOCALDESTINATION, "top" ); // trygve

        actionContext.setAction( SinkActionContext.SECTION_TITLE_4 );
    }

    /**
     * {@inheritDoc}
     */
    public void section5_()
    {
        writeEndElement(); // ElementTags.SECTION

        numberDepth--;
        depth = 1;

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void section5()
    {
        numberDepth++;
        depth = 1;

        writeStartElement( ElementTags.SECTION );
        writeAddAttribute( ElementTags.NUMBERDEPTH, numberDepth );
        writeAddAttribute( ElementTags.DEPTH, depth );
        writeAddAttribute( ElementTags.INDENT, "0.0" );

        lineBreak();

        actionContext.setAction( SinkActionContext.SECTION_5 );
    }

    /**
     * {@inheritDoc}
     */
    public void sectionTitle5_()
    {
        writeEndElement(); // ElementTags.TITLE

        font.setSize( ITextFont.DEFAULT_FONT_SIZE );
        bold_();

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void sectionTitle5()
    {
        font.setSize( ITextFont.getSectionFontSize( 5 ) );
        font.setColor( Color.BLACK );
        bold();

        writeStartElement( ElementTags.TITLE );
        writeAddAttribute( ElementTags.LEADING, DEFAULT_SECTION_TITLE_LEADING );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );
//        writeAddAttribute( ElementTags.LOCALDESTINATION, "top" ); // trygve

        actionContext.setAction( SinkActionContext.SECTION_TITLE_5 );
    }

    // ----------------------------------------------------------------------
    // Paragraph
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void paragraph_()
    {
        // Special case
        if ( ( actionContext.getCurrentAction() == SinkActionContext.LIST_ITEM )
            || ( actionContext.getCurrentAction() == SinkActionContext.NUMBERED_LIST_ITEM )
            || ( actionContext.getCurrentAction() == SinkActionContext.DEFINITION ) )
        {
            return;
        }

        writeEndElement(); // ElementTags.PARAGRAPH

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void paragraph()
    {
        // Special case
        if ( ( actionContext.getCurrentAction() == SinkActionContext.LIST_ITEM )
            || ( actionContext.getCurrentAction() == SinkActionContext.NUMBERED_LIST_ITEM )
            || ( actionContext.getCurrentAction() == SinkActionContext.DEFINITION ) )
        {
            return;
        }

        writeStartElement( ElementTags.PARAGRAPH );
        writeStartElement( ElementTags.NEWLINE );
        writeEndElement();

        actionContext.setAction( SinkActionContext.PARAGRAPH );
    }

    // ----------------------------------------------------------------------
    // Lists
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void list_()
    {
        writeEndElement(); // ElementTags.LIST

        writeEndElement(); // ElementTags.CHUNK

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void list()
    {
        writeStartElement( ElementTags.CHUNK );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );

        writeStartElement( ElementTags.LIST );
        writeAddAttribute( ElementTags.NUMBERED, Boolean.FALSE.toString() );
        writeAddAttribute( ElementTags.SYMBOLINDENT, "15" );

        actionContext.setAction( SinkActionContext.LIST );
    }

    /**
     * {@inheritDoc}
     */
    public void listItem_()
    {
        writeEndElement(); // ElementTags.LISTITEM

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void listItem()
    {
        writeStartElement( ElementTags.LISTITEM );
        writeAddAttribute( ElementTags.INDENTATIONLEFT, "20.0" );

        actionContext.setAction( SinkActionContext.LIST_ITEM );
    }

    /**
     * {@inheritDoc}
     */
    public void numberedList_()
    {
        writeEndElement(); // ElementTags.LIST

        writeEndElement(); // ElementTags.CHUNK

        actionContext.release();
    }

    /** {@inheritDoc} */
    public void numberedList( int numbering )
    {
        writeStartElement( ElementTags.CHUNK );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );

        writeStartElement( ElementTags.LIST );
        writeAddAttribute( ElementTags.NUMBERED, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.SYMBOLINDENT, "20" );

        switch ( numbering )
        {
            case Sink.NUMBERING_UPPER_ALPHA:
                writeAddAttribute( ElementTags.LETTERED, Boolean.TRUE.toString() );
                writeAddAttribute( ElementTags.FIRST, 'A' );
                break;

            case Sink.NUMBERING_LOWER_ALPHA:
                writeAddAttribute( ElementTags.LETTERED, Boolean.TRUE.toString() );
                writeAddAttribute( ElementTags.FIRST, 'a' );
                break;

            // TODO Doesn't work
            case Sink.NUMBERING_UPPER_ROMAN:
                writeAddAttribute( ElementTags.LETTERED, Boolean.TRUE.toString() );
                writeAddAttribute( ElementTags.FIRST, 'I' );
                break;

            case Sink.NUMBERING_LOWER_ROMAN:
                writeAddAttribute( ElementTags.LETTERED, Boolean.TRUE.toString() );
                writeAddAttribute( ElementTags.FIRST, 'i' );
                break;

            case Sink.NUMBERING_DECIMAL:
            default:
                writeAddAttribute( ElementTags.LETTERED, Boolean.FALSE.toString() );
        }

        actionContext.setAction( SinkActionContext.NUMBERED_LIST );
    }

    /**
     * {@inheritDoc}
     */
    public void numberedListItem_()
    {
        writeEndElement(); // ElementTags.LISTITEM

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void numberedListItem()
    {
        writeStartElement( ElementTags.LISTITEM );
        writeAddAttribute( ElementTags.INDENTATIONLEFT, "20" );

        actionContext.setAction( SinkActionContext.NUMBERED_LIST_ITEM );
    }

    /**
     * {@inheritDoc}
     */
    public void definitionList_()
    {
        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void definitionList()
    {
        lineBreak();

        actionContext.setAction( SinkActionContext.DEFINITION_LIST );
    }

    /**
     * {@inheritDoc}
     */
    public void definedTerm_()
    {
        font.setSize( ITextFont.DEFAULT_FONT_SIZE );
        bold_();

        writeEndElement(); // ElementTags.CHUNK

        actionContext.release();

        lineBreak();
    }

    /**
     * {@inheritDoc}
     */
    public void definedTerm()
    {
        font.setSize( ITextFont.DEFAULT_FONT_SIZE + 2 );
        bold();

        writeStartElement( ElementTags.CHUNK );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );

        actionContext.setAction( SinkActionContext.DEFINED_TERM );
    }

    /**
     * {@inheritDoc}
     */
    public void definition_()
    {
        writeEndElement(); // ElementTags.CHUNK

        actionContext.release();

        lineBreak();
    }

    /**
     * {@inheritDoc}
     */
    public void definition()
    {
        writeStartElement( ElementTags.CHUNK );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );


        writeStartElement( ElementTags.CHUNK );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );

        // We need to add a non break space first to display empty string
        write( "\u00A0" + StringUtils.repeat( " ", 16 ), false, false );

        writeEndElement(); // ElementTags.CHUNK

        actionContext.setAction( SinkActionContext.DEFINITION );
    }

    /**
     * {@inheritDoc}
     */
    public void definitionListItem_()
    {
        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void definitionListItem()
    {
        actionContext.setAction( SinkActionContext.DEFINITION_LIST_ITEM );
    }

    // ----------------------------------------------------------------------
    //  Tables
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void table_()
    {
        if ( tableCaptionXMLWriter != null )
        {
            tableCaptionXMLWriter = null;

            writeEndElement(); // ElementTags.TABLE

            writeEndElement(); // ElementTags.CHUNK

            writeStartElement( ElementTags.PARAGRAPH );
            writeAddAttribute( ElementTags.ALIGN, ElementTags.ALIGN_CENTER );

            write( tableCaptionWriter.toString(), true );

            writeEndElement(); // ElementTags.PARAGRAPH

            tableCaptionWriter = null;
        }
        else
        {
            writeEndElement(); // ElementTags.TABLE

            writeEndElement(); // ElementTags.CHUNK
        }
        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void table()
    {
        writeStartElement( ElementTags.CHUNK );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );

        writeStartElement( ElementTags.TABLE );
        writeAddAttribute( ElementTags.LEFT, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.RIGHT, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.TOP, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.BOTTOM, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.ALIGN, ElementTags.ALIGN_CENTER );
        writeAddAttribute( ElementTags.WIDTH, "100.0%" );
        writeAddAttribute( ElementTags.TABLEFITSPAGE, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.CELLSFITPAGE, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.CELLPADDING, "10" );
        //writeAddAttribute( ElementTags.COLUMNS, "2" );

        actionContext.setAction( SinkActionContext.TABLE );
    }

    /**
     * {@inheritDoc}
     */
    public void tableCaption_()
    {
        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void tableCaption()
    {
        tableCaptionWriter = new StringWriter();
        tableCaptionXMLWriter = new PrettyPrintXMLWriter( tableCaptionWriter );
        actionContext.setAction( SinkActionContext.TABLE_CAPTION );
    }

    /**
     * {@inheritDoc}
     */
    public void tableCell_()
    {
        writeEndElement(); // ElementTags.CELL

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void tableCell()
    {
        writeStartElement( ElementTags.CELL );
        writeAddAttribute( ElementTags.LEFT, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.RIGHT, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.TOP, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.BOTTOM, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.HORIZONTALALIGN, ElementTags.ALIGN_LEFT );

        actionContext.setAction( SinkActionContext.TABLE_CELL );
    }

    /** {@inheritDoc} */
    public void tableCell( String width )
    {
        actionContext.setAction( SinkActionContext.TABLE_CELL );
    }

    /**
     * {@inheritDoc}
     */
    public void tableHeaderCell_()
    {
        writeEndElement(); // ElementTags.CELL

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void tableHeaderCell()
    {
        writeStartElement( ElementTags.CELL );
        writeAddAttribute( ElementTags.LEFT, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.RIGHT, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.TOP, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.BOTTOM, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.HEADER, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.BGRED, Color.GRAY.getRed() );
        writeAddAttribute( ElementTags.BGBLUE, Color.GRAY.getBlue() );
        writeAddAttribute( ElementTags.BGGREEN, Color.GRAY.getGreen() );
        writeAddAttribute( ElementTags.HORIZONTALALIGN, ElementTags.ALIGN_CENTER );

        actionContext.setAction( SinkActionContext.TABLE_HEADER_CELL );
    }

    /** {@inheritDoc} */
    public void tableHeaderCell( String width )
    {
        actionContext.setAction( SinkActionContext.TABLE_HEADER_CELL );
    }

    /**
     * {@inheritDoc}
     */
    public void tableRow_()
    {
        writeEndElement(); // ElementTags.ROW

        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void tableRow()
    {
        writeStartElement( ElementTags.ROW );

        actionContext.setAction( SinkActionContext.TABLE_ROW );
    }

    /**
     * {@inheritDoc}
     */
    public void tableRows_()
    {
        //writeEndElement(); // ElementTags.TABLE

        actionContext.release();
    }

    /** {@inheritDoc} */
    public void tableRows( int[] justification, boolean grid )
    {
        // ElementTags.TABLE
        writeAddAttribute( ElementTags.COLUMNS, justification.length );

        actionContext.setAction( SinkActionContext.TABLE_ROWS );
    }

    // ----------------------------------------------------------------------
    // Verbatim
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void verbatim_()
    {
        writeEndElement(); // ElementTags.CELL

        writeEndElement(); // ElementTags.ROW

        writeEndElement(); // ElementTags.TABLE

        writeEndElement(); // ElementTags.CHUNK

        actionContext.release();
    }

    /** {@inheritDoc} */
    public void verbatim( boolean boxed )
    {
        // Always boxed
        writeStartElement( ElementTags.CHUNK );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );

        writeStartElement( ElementTags.TABLE );
        writeAddAttribute( ElementTags.COLUMNS, "1" );
        writeAddAttribute( ElementTags.LEFT, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.RIGHT, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.TOP, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.BOTTOM, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.ALIGN, ElementTags.ALIGN_CENTER );
        writeAddAttribute( ElementTags.TABLEFITSPAGE, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.CELLSFITPAGE, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.CELLPADDING, "10" );
        writeAddAttribute( ElementTags.WIDTH, "100.0%" );

        writeStartElement( ElementTags.ROW );

        writeStartElement( ElementTags.CELL );
        writeAddAttribute( ElementTags.LEFT, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.RIGHT, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.TOP, Boolean.TRUE.toString() );
        writeAddAttribute( ElementTags.BOTTOM, Boolean.TRUE.toString() );

        actionContext.setAction( SinkActionContext.VERBATIM );
    }

    // ----------------------------------------------------------------------
    // Figures
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void figure_()
    {
        writeEndElement(); // ElementTags.IMAGE

        writeEndElement(); // ElementTags.CHUNK

        actionContext.release();

        figureDefined = false;
    }

    /**
     * {@inheritDoc}
     */
    public void figure()
    {
        figureDefined = true;

        writeStartElement( ElementTags.CHUNK );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );

        writeStartElement( ElementTags.IMAGE );

        actionContext.setAction( SinkActionContext.FIGURE );
    }

    /**
     * {@inheritDoc}
     */
    public void figureCaption_()
    {
        actionContext.release();
    }

    /**
     * {@inheritDoc}
     */
    public void figureCaption()
    {
        actionContext.setAction( SinkActionContext.FIGURE_CAPTION );
    }

    /**
     * If the <code>name</code> is a relative link, the internal link will used a System property
     * <code>itext.basedir</code>, or the class loader.
     * {@inheritDoc}
     */
    public void figureGraphics( String name )
    {
        String urlName = null;
        File nameFile = null;
        if ( ( name.toLowerCase( Locale.ENGLISH ).startsWith( "http://" ) )
            || ( name.toLowerCase( Locale.ENGLISH ).startsWith( "https://" ) ) )
        {
            urlName = name;
        }
        else
        {
            if ( System.getProperty( "itext.basedir" ) != null )
            {
                try
                {
                    nameFile = new File( System.getProperty( "itext.basedir" ), name );
                    urlName = nameFile.toURI().toURL().toString();
                }
                catch ( MalformedURLException e )
                {
                    getLog().error( "MalformedURLException: " + e.getMessage(), e );
                }
            }
            else
            {
                if ( getClassLoader() != null )
                {
                    if ( getClassLoader().getResource( name ) != null )
                    {
                        urlName = getClassLoader().getResource( name ).toString();
                    }
                }
                else
                {
                    if ( ITextSink.class.getClassLoader().getResource( name ) != null )
                    {
                        urlName = ITextSink.class.getClassLoader().getResource( name ).toString();
                    }
                }
            }
        }

        if ( urlName == null )
        {
            String msg =
                "No image '" + name
                    + "' found in the class loader. Try to call setClassLoader(ClassLoader) before.";
            logMessage( "imageNotFound", msg );

            return;
        }

        if ( nameFile != null && !nameFile.exists() )
        {
            String msg = "No image '" + nameFile + "' found in your system, check the path.";
            logMessage( "imageNotFound", msg );

            return;
        }

        boolean figureCalled = figureDefined;
        if ( !figureCalled )
        {
            figure();
        }

        float width = 0;
        float height = 0;
        try
        {
            Image image = Image.getInstance( new URL( urlName ) );
            image.scaleToFit( ITextUtil.getDefaultPageSize().width() / 2, ITextUtil.getDefaultPageSize().height() / 2 );
            width = image.plainWidth();
            height = image.plainHeight();
        }
        catch ( BadElementException e )
        {
            getLog().error( "BadElementException: " + e.getMessage(), e );
        }
        catch ( MalformedURLException e )
        {
            getLog().error( "MalformedURLException: " + e.getMessage(), e );
        }
        catch ( IOException e )
        {
            getLog().error( "IOException: " + e.getMessage(), e );
        }

        writeAddAttribute( ElementTags.URL, urlName );
        writeAddAttribute( ElementTags.ALIGN, ElementTags.ALIGN_MIDDLE );
        writeAddAttribute( ElementTags.PLAINWIDTH, String.valueOf( width ) );
        writeAddAttribute( ElementTags.PLAINHEIGHT, String.valueOf( height ) );

        actionContext.setAction( SinkActionContext.FIGURE_GRAPHICS );

        if ( !figureCalled )
        {
            figure_();
        }
    }

    // ----------------------------------------------------------------------
    // Fonts
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void inline()
    {
        inline( null );
    }

    /** {@inheritDoc} */
    public void inline( SinkEventAttributes attributes )
    {
        List<String> tags = new ArrayList<>();

        if ( attributes != null )
        {

            if ( attributes.containsAttribute( SinkEventAttributes.SEMANTICS, "italic" ) )
            {
                font.addItalic();
                tags.add( 0, "italic" );
            }

            if ( attributes.containsAttribute( SinkEventAttributes.SEMANTICS, "bold" ) )
            {
                font.addBold();
                tags.add( 0, "bold" );
            }

            if ( attributes.containsAttribute( SinkEventAttributes.SEMANTICS, "code" ) )
            {
                font.setMonoSpaced( true );
                tags.add( 0, "code" );
            }

        }

        inlineStack.push( tags );
    }

    /**
     * {@inheritDoc}
     */
    public void inline_()
    {
        for ( String tag: inlineStack.pop() )
        {
            if ( "italic".equals( tag ) )
            {
                font.removeItalic();
            }
            else if ( "bold".equals( tag ) )
            {
                font.removeBold();
            }
            else if ( "code".equals( tag ) )
            {
                font.setMonoSpaced( false );
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void bold_()
    {
        inline_();
    }

    /**
     * {@inheritDoc}
     */
    public void bold()
    {
        inline( SinkEventAttributeSet.Semantics.BOLD );
    }

    /**
     * {@inheritDoc}
     */
    public void italic_()
    {
        inline_();
    }

    /**
     * {@inheritDoc}
     */
    public void italic()
    {
        inline( SinkEventAttributeSet.Semantics.ITALIC );
    }

    /**
     * {@inheritDoc}
     */
    public void monospaced_()
    {
        inline_();
    }

    /**
     * {@inheritDoc}
     */
    public void monospaced()
    {
        inline( SinkEventAttributeSet.Semantics.CODE );
    }

    // ----------------------------------------------------------------------
    // Links
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void link_()
    {
        writeEndElement(); // ElementTags.ANCHOR

        font.setColor( Color.BLACK );
        font.removeUnderlined();

        actionContext.release();
    }

    /** {@inheritDoc} */
    public void link( String name )
    {
        if ( name == null )
        {
            throw new NullPointerException( "Link name cannot be null!" );
        }

        font.setColor( Color.BLUE );
        font.addUnderlined();

        writeStartElement( ElementTags.ANCHOR );
        if ( StringUtils.isNotEmpty( name ) && name.startsWith( "#" ) && StringUtils.isNotEmpty( header.getTitle() ) )
        {
            name = "#" + DoxiaUtils.encodeId( header.getTitle(), true ) + "_" + name.substring( 1 );
        }
        writeAddAttribute( ElementTags.REFERENCE, HtmlTools.escapeHTML( name ) );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );

        actionContext.setAction( SinkActionContext.LINK );
    }

    /**
     * {@inheritDoc}
     */
    public void anchor_()
    {
        if ( !anchorDefined )
        {
            // itext needs a defined local destination, we put an invisible text
            writeAddAttribute( ElementTags.BLUE, "255" );
            writeAddAttribute( ElementTags.GREEN, "255" );
            writeAddAttribute( ElementTags.RED, "255" );

            write( "_" );
        }

        anchorDefined = false;

        writeEndElement(); // ElementTags.ANCHOR

        actionContext.release();
    }

    /** {@inheritDoc} */
    public void anchor( String name )
    {
        if ( name == null )
        {
            throw new NullPointerException( "Anchor name cannot be null!" );
        }

        if ( StringUtils.isNotEmpty( header.getTitle() ) )
        {
            name = header.getTitle() + "_" + name;
        }
        String id = name;

        if ( !DoxiaUtils.isValidId( id ) )
        {
            id = DoxiaUtils.encodeId( name, true );

            String msg = "Modified invalid link: '" + name + "' to '" + id + "'";
            logMessage( "modifiedLink", msg );
        }

        writeStartElement( ElementTags.ANCHOR );
        writeAddAttribute( ElementTags.NAME, id );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );

        actionContext.setAction( SinkActionContext.ANCHOR );
    }

    // ----------------------------------------------------------------------
    // Misc
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void lineBreak()
    {
        // Special case for the header
        if ( ( actionContext.getCurrentAction() == SinkActionContext.AUTHOR )
            || ( actionContext.getCurrentAction() == SinkActionContext.DATE )
            || ( actionContext.getCurrentAction() == SinkActionContext.TITLE ) )
        {
            return;
        }

        writeStartElement( ElementTags.NEWLINE );
        writeEndElement();
    }

    /**
     * {@inheritDoc}
     */
    public void nonBreakingSpace()
    {
        write( " " );
    }

    /**
     * {@inheritDoc}
     */
    public void pageBreak()
    {
        writeStartElement( ElementTags.NEWPAGE );
        writeEndElement();
    }

    /**
     * {@inheritDoc}
     */
    public void horizontalRule()
    {
        writeStartElement( ElementTags.PARAGRAPH );
        writeAddAttribute( ElementTags.BLUE, "255" );
        writeAddAttribute( ElementTags.GREEN, "255" );
        writeAddAttribute( ElementTags.RED, "255" );
        write( "_" );
        writeEndElement();

        writeStartElement( ElementTags.PARAGRAPH );
        writeStartElement( ElementTags.HORIZONTALRULE );
        writeEndElement();
        writeEndElement();
    }

    // ----------------------------------------------------------------------
    // Text
    // ----------------------------------------------------------------------

    /** {@inheritDoc} */
    public void rawText( String text )
    {
        writeStartElement( ElementTags.CHUNK );
        writeAddAttribute( ElementTags.FONT, font.getFontName() );
        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );

        write( text, false );

        writeEndElement(); // ElementTags.CHUNK
    }

    /** {@inheritDoc} */
    public void text( String text )
    {
        if ( StringUtils.isEmpty( text ) )
        {
            return;
        }

        switch ( actionContext.getCurrentAction() )
        {
            case SinkActionContext.AUTHOR:
                header.addAuthor( text );
                break;

            case SinkActionContext.DATE:
                header.setDate( text );
                break;

            case SinkActionContext.TITLE:
                header.setTitle( text );
                break;

            case SinkActionContext.TABLE_CAPTION:
                this.tableCaptionXMLWriter.writeText( text );
                break;

            case SinkActionContext.VERBATIM:
                // Used to preserve indentation and formating
                LineNumberReader lnr = new LineNumberReader( new StringReader( text ) );
                String line;
                try
                {
                    while ( ( line = lnr.readLine() ) != null )
                    {
                        writeStartElement( ElementTags.CHUNK );
                        writeAddAttribute( ElementTags.FONT, font.getFontName() );
                        writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
                        writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
                        writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
                        writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
                        writeAddAttribute( ElementTags.RED, font.getFontColorRed() );

                        write( "<![CDATA[", true );
                        // Special case
                        line = StringUtils.replace( line, "<![CDATA[", "< ![CDATA[" );
                        line = StringUtils.replace( line, "]]>", "]] >" );
                        write( line, true, false );
                        write( "]]>", true );

                        writeEndElement();
                        lineBreak();
                    }
                }
                catch ( IOException e )
                {
                    throw new RuntimeException( "IOException: ", e );
                }
                break;

            case SinkActionContext.FIGURE_CAPTION:
                writeAddAttribute( ElementTags.ALT, text );
                break;

            case SinkActionContext.SECTION_TITLE:
            case SinkActionContext.SECTION_1:
            case SinkActionContext.SECTION_2:
            case SinkActionContext.SECTION_3:
            case SinkActionContext.SECTION_4:
            case SinkActionContext.SECTION_5:
            case SinkActionContext.FIGURE:
            case SinkActionContext.FIGURE_GRAPHICS:
            case SinkActionContext.TABLE_ROW:
            case SinkActionContext.TABLE:
            case SinkActionContext.HEAD:
            case SinkActionContext.UNDEFINED:
                break;

            case SinkActionContext.ANCHOR:
                anchorDefined = true;
            case SinkActionContext.PARAGRAPH:
            case SinkActionContext.LINK:
            case SinkActionContext.TABLE_CELL:
            case SinkActionContext.TABLE_HEADER_CELL:
            case SinkActionContext.DEFINITION:
            case SinkActionContext.DEFINED_TERM:
            case SinkActionContext.NUMBERED_LIST_ITEM:
            case SinkActionContext.LIST_ITEM:
            case SinkActionContext.SECTION_TITLE_5:
            case SinkActionContext.SECTION_TITLE_4:
            case SinkActionContext.SECTION_TITLE_3:
            case SinkActionContext.SECTION_TITLE_2:
            case SinkActionContext.SECTION_TITLE_1:
            default:
                writeStartElement( ElementTags.CHUNK );
                writeAddAttribute( ElementTags.FONT, font.getFontName() );
                writeAddAttribute( ElementTags.SIZE, font.getFontSize() );
                writeAddAttribute( ElementTags.STYLE, font.getFontStyle() );
                writeAddAttribute( ElementTags.BLUE, font.getFontColorBlue() );
                writeAddAttribute( ElementTags.GREEN, font.getFontColorGreen() );
                writeAddAttribute( ElementTags.RED, font.getFontColorRed() );

                write( text );

                writeEndElement(); // ElementTags.CHUNK
        }
    }

    /**
     * {@inheritDoc}
     *
     * Unkown events just log a warning message but are ignored otherwise.
     * @see org.apache.maven.doxia.sink.Sink#unknown(String,Object[],SinkEventAttributes)
     */
    public void unknown( String name, Object[] requiredParams, SinkEventAttributes attributes )
    {
        String msg = "Unknown Sink event: '" + name + "', ignoring!";
        logMessage( "unknownEvent", msg );
    }

    /**
     * {@inheritDoc}
     */
    protected void init()
    {
        super.init();

        this.actionContext = new SinkActionContext();
        this.font = new ITextFont();
        this.header = new ITextHeader();

        this.numberDepth = 1;
        this.depth = 0;
        this.tableCaptionWriter = null;
        this.tableCaptionXMLWriter = null;
        this.anchorDefined = false;
        this.figureDefined = false;
        this.warnMessages = null;
    }

    /**
     * Convenience method to write a starting element.
     *
     * @param tag the name of the tag
     */
    private void writeStartElement( String tag )
    {
        if ( tableCaptionXMLWriter == null )
        {
            xmlWriter.startElement( tag );
        }
        else
        {
            tableCaptionXMLWriter.startElement( tag );
        }
    }

    /**
     * Convenience method to write a key-value pair.
     *
     * @param key the name of an attribute
     * @param value the value of an attribute
     */
    private void writeAddAttribute( String key, String value )
    {
        if ( tableCaptionXMLWriter == null )
        {
            xmlWriter.addAttribute( key, value );
        }
        else
        {
            tableCaptionXMLWriter.addAttribute( key, value );
        }
    }

    /**
     * Convenience method to write a key-value pair.
     *
     * @param key the name of an attribute
     * @param value the value of an attribute
     */
    private void writeAddAttribute( String key, int value )
    {
        if ( tableCaptionXMLWriter == null )
        {
            xmlWriter.addAttribute( key, String.valueOf( value ) );
        }
        else
        {
            tableCaptionXMLWriter.addAttribute( key, String.valueOf( value ) );
        }
    }

    /**
     * Convenience method to write an end element.
     */
    private void writeEndElement()
    {
        if ( tableCaptionXMLWriter == null )
        {
            xmlWriter.endElement();
        }
        else
        {
            tableCaptionXMLWriter.endElement();
        }
    }

    /**
     * {@inheritDoc}
     *
     * Convenience method to write a String
     */
    protected void write( String aString )
    {
        write( aString, false );
    }

    /**
     * Convenience method to write a String depending the escapeHtml flag
     *
     * @param aString
     * @param escapeHtml
     */
    private void write( String aString, boolean escapeHtml )
    {
        write( aString, escapeHtml, true );
    }

    /**
     * Convenience method to write a String depending the escapeHtml flag
     *
     * @param aString
     * @param escapeHtml
     * @param trim
     */
    private void write( String aString, boolean escapeHtml, boolean trim )
    {
        if ( aString == null )
        {
            return;
        }

        if ( trim )
        {
            aString = StringUtils.replace( aString, "\n", "" );

            LineNumberReader lnr = new LineNumberReader( new StringReader( aString ) );
            StringBuilder sb = new StringBuilder();
            String line;
            try
            {
                while ( ( line = lnr.readLine() ) != null )
                {
                    sb.append( beautifyPhrase( line.trim() ) );
                    sb.append( " " );
                }

                aString = sb.toString();
            }
            catch ( IOException e )
            {
                // nop
            }
            if ( aString.trim().length() == 0 )
            {
                return;
            }
        }
        if ( escapeHtml )
        {
            if ( tableCaptionXMLWriter == null )
            {
                xmlWriter.writeMarkup( aString );
            }
            else
            {
                tableCaptionXMLWriter.writeMarkup( aString );
            }
        }
        else
        {
            if ( tableCaptionXMLWriter == null )
            {
                xmlWriter.writeText( aString );
            }
            else
            {
                tableCaptionXMLWriter.writeText( aString );
            }
        }
    }

    /**
     * Convenience method to return a beautify phrase, i.e. one space between words.
     *
     * @param aString
     * @return a String with only one space between words
     */
    private static String beautifyPhrase( String aString )
    {
        String[] strings = StringUtils.split( aString, " " );
        StringBuilder sb = new StringBuilder();
        for ( String string : strings )
        {
            if ( string.trim().length() != 0 )
            {
                sb.append( string.trim() );
                sb.append( " " );
            }
        }

        return sb.toString().trim();
    }

    /**
     * If debug mode is enabled, log the <code>msg</code> as is, otherwise add unique msg in <code>warnMessages</code>.
     *
     * @param key not null
     * @param msg not null
     * @see #close()
     * @since 1.1.1
     */
    private void logMessage( String key, String msg )
    {
        msg = "[iText Sink] " + msg;
        if ( getLog().isDebugEnabled() )
        {
            getLog().debug( msg );

            return;
        }

        if ( warnMessages == null )
        {
            warnMessages = new HashMap<>();
        }

        Set<String> set = warnMessages.get( key );
        if ( set == null )
        {
            set = new TreeSet<>();
        }
        set.add( msg );
        warnMessages.put( key, set );
    }
}
