/*************************************************************************
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 * 
 * Copyright 2000, 2010 Oracle and/or its affiliates.
 *
 * OpenOffice.org - a multi-platform office productivity suite
 *
 * This file is part of OpenOffice.org.
 *
 * OpenOffice.org is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * OpenOffice.org 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 Lesser General Public License version 3 for more details
 * (a copy is included in the LICENSE file that accompanied this code).
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with OpenOffice.org.  If not, see
 * <http://www.openoffice.org/license.html>
 * for a copy of the LGPLv3 License.
 *
 ************************************************************************/
package com.sun.star.report.pentaho.output.spreadsheet;

import com.sun.star.report.DataSourceFactory;
import com.sun.star.report.ImageService;
import com.sun.star.report.InputRepository;
import com.sun.star.report.OfficeToken;
import com.sun.star.report.OutputRepository;
import com.sun.star.report.pentaho.OfficeNamespaces;
import com.sun.star.report.pentaho.PentahoReportEngineMetaData;
import com.sun.star.report.pentaho.model.OfficeMasterPage;
import com.sun.star.report.pentaho.model.OfficeMasterStyles;
import com.sun.star.report.pentaho.model.OfficeStyle;
import com.sun.star.report.pentaho.model.OfficeStyles;
import com.sun.star.report.pentaho.model.OfficeStylesCollection;
import com.sun.star.report.pentaho.model.PageSection;
import com.sun.star.report.pentaho.output.OfficeDocumentReportTarget;
import com.sun.star.report.pentaho.output.StyleUtilities;
import com.sun.star.report.pentaho.output.text.MasterPageFactory;
import com.sun.star.report.pentaho.styles.LengthCalculator;

import java.io.IOException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.jfree.layouting.input.style.values.CSSNumericType;
import org.jfree.layouting.input.style.values.CSSNumericValue;
import org.jfree.layouting.util.AttributeMap;
import org.jfree.report.DataFlags;
import org.jfree.report.DataSourceException;
import org.jfree.report.JFreeReportInfo;
import org.jfree.report.ReportProcessingException;
import org.jfree.report.flow.ReportJob;
import org.jfree.report.flow.ReportStructureRoot;
import org.jfree.report.flow.ReportTargetUtil;
import org.jfree.report.structure.Element;
import org.jfree.report.structure.Section;
import org.jfree.report.util.IntegerCache;

import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
import org.pentaho.reporting.libraries.xmlns.common.AttributeList;
import org.pentaho.reporting.libraries.xmlns.writer.XmlWriter;
import org.pentaho.reporting.libraries.xmlns.writer.XmlWriterSupport;


/**
 * Creation-Date: 03.11.2007
 *
 * @author Michael D'Amour
 */
public class SpreadsheetRawReportTarget extends OfficeDocumentReportTarget
{

    private static final String[] FOPROPS = new String[]
    {
        "letter-spacing", "font-variant", "text-transform"
    };
    private static final String NUMBERCOLUMNSSPANNED = "number-columns-spanned";
    private static final String[] STYLEPROPS = new String[]
    {
        "text-combine", "font-pitch-complex", "text-rotation-angle", "font-name", "text-blinking", "letter-kerning", "text-combine-start-char", "text-combine-end-char", "text-position", "text-scale"
    };
    private static final int CELL_WIDTH_FACTOR = 10000;
    private static final String TRANSPARENT = "transparent";
    private boolean paragraphFound = false;
    private boolean paragraphHandled = false;

    /**
     * This class represents a column boundary, not in width, but it's actual boundary location. One of the motivations
     * for creating this class was to be able to record the boundaries for each incoming table while consuming as few
     * objects/memory as possible.
     */
    private static class ColumnBoundary implements Comparable
    {

        private final Set tableIndices;
        private final long boundary;

        private ColumnBoundary(final long boundary)
        {
            this.tableIndices = new HashSet();
            this.boundary = boundary;
        }

        public void addTableIndex(final int table)
        {
            tableIndices.add(IntegerCache.getInteger(table));
        }

        public float getBoundary()
        {
            return boundary;
        }

        public boolean isContainedByTable(final int table)
        {
            final Integer index = IntegerCache.getInteger(table);
            return tableIndices.contains(index);
        }

        public int compareTo(final Object arg0)
        {
            if (arg0.equals(this))
            {
                return 0;
            }
            if (arg0 instanceof ColumnBoundary)
            {
                if (boundary > ((ColumnBoundary) arg0).boundary)
                {
                    return 1;
                }
                else
                {
                    return -1;
                }
            }
            return 1;
        }

        public boolean equals(final Object obj)
        {
            return obj instanceof ColumnBoundary && ((ColumnBoundary) obj).boundary == boundary;
        }

        public int hashCode()
        {
            assert false : "hashCode not designed";
            return 42; // any arbitrary constant will do
        }
    }
    private String tableBackgroundColor; // null means transparent ...
    private static final ColumnBoundary[] EMPTY_COLBOUNDS = new ColumnBoundary[0];
    private boolean elementBoundaryCollectionPass;
    private boolean oleHandled;
    private final List columnBoundaryList;
    private long currentRowBoundaryMarker;
    private ColumnBoundary[] sortedBoundaryArray;
    private ColumnBoundary[] boundariesForTableArray;
    private int tableCounter;
    private int columnCounter;
    private int columnSpanCounter;
    private int currentSpan = 0;
    private String unitsOfMeasure;
    final private List shapes;
    final private List ole;
    final private List rowHeights;

    public SpreadsheetRawReportTarget(final ReportJob reportJob,
            final ResourceManager resourceManager,
            final ResourceKey baseResource,
            final InputRepository inputRepository,
            final OutputRepository outputRepository,
            final String target,
            final ImageService imageService,
            final DataSourceFactory dataSourceFactory)
            throws ReportProcessingException
    {
        super(reportJob, resourceManager, baseResource, inputRepository, outputRepository, target, imageService, dataSourceFactory);
        columnBoundaryList = new ArrayList();
        elementBoundaryCollectionPass = true;
        rowHeights = new ArrayList();
        shapes = new ArrayList();
        ole = new ArrayList();
        oleHandled = false;
    }

    public void startOther(final AttributeMap attrs) throws DataSourceException, ReportProcessingException
    {
        if (ReportTargetUtil.isElementOfType(JFreeReportInfo.REPORT_NAMESPACE, OfficeToken.OBJECT_OLE, attrs))
        {
            if (isElementBoundaryCollectionPass() && getCurrentRole() != ROLE_TEMPLATE)
            {
                ole.add(attrs);
            }
            oleHandled = true;
            return;
        }
        final String namespace = ReportTargetUtil.getNamespaceFromAttribute(attrs);
        if (isRepeatingSection() || isFilteredNamespace(namespace))
        {
            return;
        }

        final String elementType = ReportTargetUtil.getElemenTypeFromAttribute(attrs);
        if (OfficeNamespaces.TEXT_NS.equals(namespace) && OfficeToken.P.equals(elementType) && !paragraphHandled)
        {
            paragraphFound = true;
            return;
        }

        if (OfficeNamespaces.DRAWING_NS.equals(namespace) && OfficeToken.FRAME.equals(elementType))
        {
            if (isElementBoundaryCollectionPass() && getCurrentRole() != ROLE_TEMPLATE)
            {
                final LengthCalculator len = new LengthCalculator();
                for (int i = 0; i < rowHeights.size(); i++)
                {
                    len.add((CSSNumericValue) rowHeights.get(i));
                    // val += ((CSSNumericValue)rowHeights.get(i)).getValue();
                }

                rowHeights.clear();
                final CSSNumericValue currentRowHeight = len.getResult();
                rowHeights.add(currentRowHeight);
                attrs.setAttribute(OfficeNamespaces.DRAWING_NS, "z-index", String.valueOf(shapes.size()));
                final String y = (String) attrs.getAttribute(OfficeNamespaces.SVG_NS, "y");
                if (y != null)
                {
                    len.add(parseLength(y));
                    final CSSNumericValue currentY = len.getResult();
                    attrs.setAttribute(OfficeNamespaces.SVG_NS, "y", currentY.getValue() + currentY.getType().getType());
                }
                shapes.add(attrs);
            }
            return;
        }
        if (oleHandled)
        {
            if (isElementBoundaryCollectionPass() && getCurrentRole() != ROLE_TEMPLATE)
            {
                ole.add(attrs);
            }
            return;
        }

        // if this is the report namespace, write out a table definition ..
        if (OfficeNamespaces.TABLE_NS.equals(namespace) && OfficeToken.TABLE.equals(elementType))
        {
            // whenever we see a new table, we increment our tableCounter
            // this is used to keep tracked of the boundary conditions per table
            tableCounter++;
        }

        if (isElementBoundaryCollectionPass())
        {
            collectBoundaryForElement(attrs);
        }
        else
        // if (!isElementBoundaryCollectionPass())
        {
            try
            {
                processElement(attrs, namespace, elementType);
            }
            catch (IOException e)
            {
                throw new ReportProcessingException(OfficeDocumentReportTarget.FAILED, e);
            }
        }
    }

    protected void startReportSection(final AttributeMap attrs, final int role) throws IOException, DataSourceException, ReportProcessingException
    {
        if ((role == OfficeDocumentReportTarget.ROLE_SPREADSHEET_PAGE_HEADER || role == OfficeDocumentReportTarget.ROLE_SPREADSHEET_PAGE_FOOTER) && (!PageSection.isPrintWithReportHeader(attrs) || !PageSection.isPrintWithReportFooter(attrs)))
        {
            startBuffering(new OfficeStylesCollection(), true);
        }
        else
        {
            super.startReportSection(attrs, role);
        }
    }

    protected void endReportSection(final AttributeMap attrs, final int role) throws IOException, DataSourceException, ReportProcessingException
    {
        if ((role == OfficeDocumentReportTarget.ROLE_SPREADSHEET_PAGE_HEADER || role == OfficeDocumentReportTarget.ROLE_SPREADSHEET_PAGE_FOOTER) && (!PageSection.isPrintWithReportHeader(attrs) || !PageSection.isPrintWithReportFooter(attrs)))
        {
            finishBuffering();
        }
        else
        {
            super.endReportSection(attrs, role);
        }
    }

    private void handleParagraph()
    {
        if (paragraphFound)
        {
            try
            {
                final XmlWriter xmlWriter = getXmlWriter();
                xmlWriter.writeTag(OfficeNamespaces.TEXT_NS, OfficeToken.P, null, XmlWriterSupport.OPEN);
                paragraphHandled = true;
                paragraphFound = false;
            }
            catch (IOException ex)
            {
                LOGGER.error("ReportProcessing failed", ex);
            }
        }
    }

    private void processElement(final AttributeMap attrs, final String namespace, final String elementType)
            throws IOException, ReportProcessingException
    {
        final XmlWriter xmlWriter = getXmlWriter();

        if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE, attrs))
        {
            // a new table means we must clear our "calculated" table boundary array cache
            boundariesForTableArray = null;

            final String tableStyle = (String) attrs.getAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME);
            if (tableStyle == null)
            {
                tableBackgroundColor = null;
            }
            else
            {
                final Object raw = StyleUtilities.queryStyle(getPredefinedStylesCollection(), OfficeToken.TABLE, tableStyle,
                        "table-properties", OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR);
                if (raw == null || TRANSPARENT.equals(raw))
                {
                    tableBackgroundColor = null;
                }
                else
                {
                    tableBackgroundColor = String.valueOf(raw);
                }
            }
            return;
        }

        if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMN, attrs) || ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMNS, attrs))
        {
            return;
        }

        // covered-table-cell elements may appear in the input from row or column spans. In the event that we hit a
        // column-span we simply ignore these elements because we are going to adjust the span to fit the uniform table.
        if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.COVERED_TABLE_CELL, attrs))
        {
            if (columnSpanCounter > 0)
            {
                columnSpanCounter--;
            }

            if (columnSpanCounter == 0)
            {
                // if we weren't expecting a covered-table-cell, let's use it, it's probably from a row-span
                columnCounter++;
                final int span = getColumnSpanForCell(tableCounter, columnCounter, 1);
                // use the calculated span for the column in the uniform table to create any additional covered-table-cell
                // elements
                for (int i = 0; i < span; i++)
                {
                    xmlWriter.writeTag(namespace, OfficeToken.COVERED_TABLE_CELL, null, XmlWriter.CLOSE);
                }
            }
            return;
        }

        if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_ROW, attrs))
        {
            // a new row means our column counter gets reset
            columnCounter = 0;
            // Lets make sure the color of the table is ok ..
            if (tableBackgroundColor != null)
            {
                final String styleName = (String) attrs.getAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME);
                final OfficeStyle style = deriveStyle(OfficeToken.TABLE_ROW, styleName);
                Element tableRowProperties = style.getTableRowProperties();
                if (tableRowProperties == null)
                {
                    tableRowProperties = new Section();
                    tableRowProperties.setNamespace(OfficeNamespaces.STYLE_NS);
                    tableRowProperties.setType("table-row-properties");
                    tableRowProperties.setAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR, tableBackgroundColor);
                    style.addNode(tableRowProperties);
                }
                else
                {
                    final Object oldValue = tableRowProperties.getAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR);
                    if (oldValue == null || TRANSPARENT.equals(oldValue))
                    {
                        tableRowProperties.setAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR, tableBackgroundColor);
                    }
                }
                attrs.setAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME, style.getStyleName());
            }
        }
        else if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_CELL, attrs))
        {
            columnCounter++;
            final String styleName = (String) attrs.getAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME);
            if (styleName != null)
            {
                final OfficeStyle cellStyle = getPredefinedStylesCollection().getStyle(OfficeToken.TABLE_CELL, styleName);
                if (cellStyle != null)
                {
                    final Section textProperties = (Section) cellStyle.getTextProperties();
                    if (textProperties != null)
                    {
                        for (String i : FOPROPS)
                        {
                            textProperties.setAttribute(OfficeNamespaces.FO_NS, i, null);
                        }
                        textProperties.setAttribute(OfficeNamespaces.TEXT_NS, "display", null);
                        for (String i : STYLEPROPS)
                        {
                            textProperties.setAttribute(OfficeNamespaces.STYLE_NS, i, null);
                        }
                    }
                    final Section props = (Section) cellStyle.getTableCellProperties();
                    if (props != null)
                    {
                        final Object raw = props.getAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR);
                        if (TRANSPARENT.equals(raw))
                        {
                            props.setAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR, null);
                            // cellStyle.removeNode(props);
                        }
                    }
                }
                attrs.setAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME, styleName);
            }

            final String numColSpanStr = (String) attrs.getAttribute(namespace, NUMBERCOLUMNSSPANNED);
            int initialColumnSpan = columnSpanCounter = 1;
            if (numColSpanStr != null)
            {
                initialColumnSpan = Integer.parseInt(numColSpanStr);
                columnSpanCounter = initialColumnSpan;
            }
            final int span = getColumnSpanForCell(tableCounter, columnCounter, initialColumnSpan);
            if (initialColumnSpan > 1)
            {
                // add the initial column span to our column counter index (subtract 1, since it is counted by default)
                columnCounter += initialColumnSpan - 1;
            }

            // if (span < initialColumnSpan)
            // {
            // // ColumnBoundary cbs[] = getBoundariesForTable(tableCounter);
            // // for (int i = 0; i < cbs.length; i++)
            // // {
            // // System.out.print(cbs[i].getBoundary() + " ");
            // // }
            // // System.out.println();
            //
            // LOGGER.error("A cell cannot span less than the declared columns: Declared=" + initialColumnSpan + " Computed="
            // + span);
            // }

            // there's no point to create number-columns-spanned attributes if we only span 1 column
            if (span > 1)
            {
                attrs.setAttribute(namespace, NUMBERCOLUMNSSPANNED, "" + span);
                currentSpan = span;
            }
            // we must also generate "covered-table-cell" elements for each column spanned
            // but we'll do this in the endElement, after we close this OfficeToken.TABLE_CELL
        }

        // All styles have to be processed or you will loose the paragraph-styles and inline text-styles.
        // ..
        performStyleProcessing(attrs);

        final AttributeList attrList = buildAttributeList(attrs);
        xmlWriter.writeTag(namespace, elementType, attrList, XmlWriter.OPEN);
        // System.out.println("elementType = " + elementType);
    }

    private void collectBoundaryForElement(final AttributeMap attrs)
    {
        if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMNS, attrs))
        {
            // A table row resets the column counter.
            resetCurrentRowBoundaryMarker();
        }
        else if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMN, attrs))
        {
            final String styleName = (String) attrs.getAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME);
            if (styleName == null)
            {
                // This should not happen, but if it does, we will ignore that cell.
                return;
            }

            final OfficeStyle style = getPredefinedStylesCollection().getStyle(OfficeToken.TABLE_COLUMN, styleName);
            if (style == null)
            {
                // Now this is very bad. It means that there is no style defined with the given name.
                return;
            }

            final Element tableColumnProperties = style.getTableColumnProperties();
            String widthStr = (String) tableColumnProperties.getAttribute("column-width");
            widthStr = widthStr.substring(0, widthStr.indexOf(getUnitsOfMeasure(widthStr)));
            final float val = Float.parseFloat(widthStr) * CELL_WIDTH_FACTOR;
            addColumnWidthToRowBoundaryMarker((long) val);
            ColumnBoundary currentRowBoundary = new ColumnBoundary(getCurrentRowBoundaryMarker());
            final List columnBoundaryList_ = getColumnBoundaryList();
            final int idx = columnBoundaryList_.indexOf(currentRowBoundary);
            if (idx == -1)
            {
                columnBoundaryList_.add(currentRowBoundary);
            }
            else
            {
                currentRowBoundary = (ColumnBoundary) columnBoundaryList_.get(idx);
            }
            currentRowBoundary.addTableIndex(tableCounter);
        }
    }

    private String getUnitsOfMeasure(final String str)
    {
        if (unitsOfMeasure == null || "".equals(unitsOfMeasure))
        {
            if (str == null || "".equals(str))
            {
                unitsOfMeasure = "cm";
                return unitsOfMeasure;
            }

            // build units of measure, set it
            int i = str.length() - 1;
            for (; i >= 0; i--)
            {
                final char c = str.charAt(i);
                if (Character.isDigit(c) || c == '.' || c == ',')
                {
                    break;
                }
            }
            unitsOfMeasure = str.substring(i + 1);
        }
        return unitsOfMeasure;
    }

    private void createTableShapes() throws ReportProcessingException
    {
        if (!shapes.isEmpty())
        {
            try
            {
                final XmlWriter xmlWriter = getXmlWriter();
                // at this point we need to generate the table-columns section based on our boundary table
                // <table:shapes>
                // <draw:frame />
                // ..
                // </table:shapes>
                xmlWriter.writeTag(OfficeNamespaces.TABLE_NS, OfficeToken.SHAPES, null, XmlWriterSupport.OPEN);


                for (int i = 0; i < shapes.size(); i++)
                {
                    final AttributeMap attrs = (AttributeMap) shapes.get(i);
                    final AttributeList attrList = buildAttributeList(attrs);
                    attrList.removeAttribute(OfficeNamespaces.DRAWING_NS, OfficeToken.STYLE_NAME);
                    xmlWriter.writeTag(OfficeNamespaces.DRAWING_NS, OfficeToken.FRAME, attrList, XmlWriterSupport.OPEN);
                    startChartProcessing((AttributeMap) ole.get(i));

                    xmlWriter.writeCloseTag();
                }
                xmlWriter.writeCloseTag();
            }
            catch (IOException e)
            {
                throw new ReportProcessingException(OfficeDocumentReportTarget.FAILED, e);
            }
        }
    }

    private void createTableColumns() throws ReportProcessingException
    {
        try
        {
            final XmlWriter xmlWriter = getXmlWriter();
            // at this point we need to generate the table-columns section based on our boundary table
            // <table-columns>
            // <table-column style-name="coX"/>
            // ..
            // </table-columns>
            // the first boundary is '0' which is a placeholder so we will ignore it
            xmlWriter.writeTag(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMNS, null, XmlWriterSupport.OPEN);

            // blow away current column styles
            // start processing at i=1 because we added a boundary for "0" which is virtual
            final ColumnBoundary[] cba = getSortedColumnBoundaryArray();
            for (int i = 1; i < cba.length; i++)
            {
                final ColumnBoundary cb = cba[i];
                float columnWidth = cb.getBoundary();
                if (i > 1)
                {
                    columnWidth -= cba[i - 1].getBoundary();
                }
                columnWidth = columnWidth / CELL_WIDTH_FACTOR;
                final OfficeStyle style = deriveStyle(OfficeToken.TABLE_COLUMN, ("co" + i + "_"));
                final Section tableColumnProperties = new Section();
                tableColumnProperties.setType("table-column-properties");
                tableColumnProperties.setNamespace(style.getNamespace());
                final String width = String.format("%f", columnWidth);
                tableColumnProperties.setAttribute(style.getNamespace(),
                        "column-width", width + getUnitsOfMeasure(null));
                style.addNode(tableColumnProperties);

                final AttributeList myAttrList = new AttributeList();
                myAttrList.setAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME, style.getStyleName());
                xmlWriter.writeTag(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_COLUMN, myAttrList, XmlWriterSupport.CLOSE);
            }
            xmlWriter.writeCloseTag();
        }
        catch (IOException e)
        {
            throw new ReportProcessingException(OfficeDocumentReportTarget.FAILED, e);
        }
    }

    protected void endOther(final AttributeMap attrs) throws DataSourceException, ReportProcessingException
    {
        if (ReportTargetUtil.isElementOfType(JFreeReportInfo.REPORT_NAMESPACE, OfficeToken.OBJECT_OLE, attrs) || oleHandled)
        {
            oleHandled = false;
            return;
        }

        if (ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_ROW, attrs) && isElementBoundaryCollectionPass() && getCurrentRole() != ROLE_TEMPLATE)
        {
            final String styleName = (String) attrs.getAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME);
            rowHeights.add(computeRowHeight(styleName));
        }

        if (isRepeatingSection() || isElementBoundaryCollectionPass())
        {
            return;
        }

        final String namespace = ReportTargetUtil.getNamespaceFromAttribute(attrs);
        if (isFilteredNamespace(namespace))
        {
            return;
        }
        final String elementType = ReportTargetUtil.getElemenTypeFromAttribute(attrs);
        if (OfficeNamespaces.DRAWING_NS.equals(namespace) && OfficeToken.FRAME.equals(elementType))
        {
            return;
        }

        // if this is the report namespace, write out a table definition ..
        if (OfficeNamespaces.TABLE_NS.equals(namespace) && (OfficeToken.TABLE.equals(elementType) || OfficeToken.COVERED_TABLE_CELL.equals(elementType) || OfficeToken.TABLE_COLUMN.equals(elementType) || OfficeToken.TABLE_COLUMNS.equals(elementType)))
        {
            return;
        }

        if (!paragraphHandled && OfficeNamespaces.TEXT_NS.equals(namespace) && OfficeToken.P.equals(elementType))
        {
            if (!paragraphHandled)
            {
                return;
            }

            paragraphHandled = false;
        }
        try
        {
            final XmlWriter xmlWriter = getXmlWriter();
            xmlWriter.writeCloseTag();
            // table-cell elements may have a number-columns-spanned attribute which indicates how many
            // 'covered-table-cell' elements we need to generate
            generateCoveredTableCells(attrs);
        }
        catch (IOException e)
        {
            throw new ReportProcessingException(OfficeDocumentReportTarget.FAILED, e);
        }
    }

    private void generateCoveredTableCells(final AttributeMap attrs) throws IOException
    {
        if (!ReportTargetUtil.isElementOfType(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE_CELL, attrs))
        {
            return;
        }

        // do this after we close the tag
        final XmlWriter xmlWriter = getXmlWriter();
        // final Object attribute = attrs.getAttribute(OfficeNamespaces.TABLE_NS,NUMBERCOLUMNSSPANNED);
        // final int span = TextUtilities.parseInt((String) attribute, 0);
        final int span = currentSpan;
        currentSpan = 0;
        for (int i = 1; i < span; i++)
        {
            xmlWriter.writeTag(OfficeNamespaces.TABLE_NS, OfficeToken.COVERED_TABLE_CELL, null, XmlWriter.CLOSE);
        }
    }

    public String getExportDescriptor()
    {
        return "raw/" + PentahoReportEngineMetaData.OPENDOCUMENT_SPREADSHEET;
    }

    // /////////////////////////////////////////////////////////////////////////
    public void processText(final String text) throws DataSourceException, ReportProcessingException
    {
        if (!(isRepeatingSection() || isElementBoundaryCollectionPass()))
        {
            handleParagraph();
            super.processText(text);
        }
    }

    public void processContent(final DataFlags value) throws DataSourceException, ReportProcessingException
    {
        if (!(isRepeatingSection() || isElementBoundaryCollectionPass()))
        {
            handleParagraph();
            super.processContent(value);
        }
    }

    protected String getStartContent()
    {
        return "spreadsheet";
    }

    protected void startContent(final AttributeMap attrs) throws IOException, DataSourceException,
            ReportProcessingException
    {
        if (!isElementBoundaryCollectionPass())
        {
            final XmlWriter xmlWriter = getXmlWriter();
            xmlWriter.writeTag(OfficeNamespaces.OFFICE_NS, getStartContent(), null, XmlWriterSupport.OPEN);

            writeNullDate();

            final AttributeMap tableAttributes = new AttributeMap();
            tableAttributes.setAttribute(JFreeReportInfo.REPORT_NAMESPACE, Element.NAMESPACE_ATTRIBUTE, OfficeNamespaces.TABLE_NS);
            tableAttributes.setAttribute(JFreeReportInfo.REPORT_NAMESPACE, Element.TYPE_ATTRIBUTE, OfficeToken.TABLE);
            tableAttributes.setAttribute(OfficeNamespaces.TABLE_NS, OfficeToken.STYLE_NAME, generateInitialTableStyle());
            tableAttributes.setAttribute(OfficeNamespaces.TABLE_NS, "name", "Report");

            performStyleProcessing(tableAttributes);

            xmlWriter.writeTag(OfficeNamespaces.TABLE_NS, OfficeToken.TABLE, buildAttributeList(tableAttributes), XmlWriterSupport.OPEN);
            createTableShapes();
            createTableColumns();
        }
    }

    private String generateInitialTableStyle() throws ReportProcessingException
    {
        final OfficeStylesCollection predefStyles = getPredefinedStylesCollection();
        final OfficeStyles commonStyles = predefStyles.getAutomaticStyles();
        if (!commonStyles.containsStyle(OfficeToken.TABLE, "Initial_Table"))
        {
            final String masterPageName = createMasterPage();

            final OfficeStyle tableStyle = new OfficeStyle();
            tableStyle.setStyleFamily(OfficeToken.TABLE);
            tableStyle.setStyleName("Initial_Table");
            tableStyle.setAttribute(OfficeNamespaces.STYLE_NS, "master-page-name", masterPageName);
            final Element tableProperties = produceFirstChild(tableStyle, OfficeNamespaces.STYLE_NS, "table-properties");
            tableProperties.setAttribute(OfficeNamespaces.FO_NS, OfficeToken.BACKGROUND_COLOR, TRANSPARENT);
            commonStyles.addStyle(tableStyle);
        }
        return "Initial_Table";
    }

    private String createMasterPage() throws ReportProcessingException
    {
        final OfficeStylesCollection predefStyles = getPredefinedStylesCollection();
        final MasterPageFactory masterPageFactory = new MasterPageFactory(predefStyles.getMasterStyles());
        final OfficeMasterPage masterPage;
        if (!masterPageFactory.containsMasterPage("Standard", null, null))
        {
            masterPage = masterPageFactory.createMasterPage("Standard", null, null);

            final CSSNumericValue zeroLength = CSSNumericValue.createValue(CSSNumericType.CM, 0);
            final String pageLayoutTemplate = masterPage.getPageLayout();
            if (pageLayoutTemplate == null)
            {
                // there is no pagelayout. Create one ..
                final String derivedLayout = masterPageFactory.createPageStyle(getGlobalStylesCollection().getAutomaticStyles(), zeroLength, zeroLength);
                masterPage.setPageLayout(derivedLayout);
            }
            else
            {
                final String derivedLayout = masterPageFactory.derivePageStyle(pageLayoutTemplate,
                        getPredefinedStylesCollection().getAutomaticStyles(),
                        getGlobalStylesCollection().getAutomaticStyles(), zeroLength, zeroLength);
                masterPage.setPageLayout(derivedLayout);
            }

            final OfficeStylesCollection officeStylesCollection = getGlobalStylesCollection();
            final OfficeMasterStyles officeMasterStyles = officeStylesCollection.getMasterStyles();
            officeMasterStyles.addMasterPage(masterPage);
        }
        else
        {
            masterPage = masterPageFactory.getMasterPage("Standard", null, null);
        }
        return masterPage.getStyleName();
    }

    protected void endContent(final AttributeMap attrs) throws IOException, DataSourceException,
            ReportProcessingException
    {
        // todo
        if (!isElementBoundaryCollectionPass())
        {
            final XmlWriter xmlWriter = getXmlWriter();
            xmlWriter.writeCloseTag();
            xmlWriter.writeCloseTag();
        }
    }

    public void endReport(final ReportStructureRoot report) throws DataSourceException, ReportProcessingException
    {
        super.endReport(report);
        setElementBoundaryCollectionPass(false);
        resetTableCounter();
        columnCounter = 0;
        copyMeta();
    }

    private boolean isElementBoundaryCollectionPass()
    {
        return elementBoundaryCollectionPass;
    }

    private void setElementBoundaryCollectionPass(final boolean elementBoundaryCollectionPass)
    {
        this.elementBoundaryCollectionPass = elementBoundaryCollectionPass;
    }

    private ColumnBoundary[] getSortedColumnBoundaryArray()
    {
        if (sortedBoundaryArray == null)
        {
            getColumnBoundaryList().add(new ColumnBoundary(0));
            sortedBoundaryArray = (ColumnBoundary[]) getColumnBoundaryList().toArray(new ColumnBoundary[getColumnBoundaryList().size()]);
            Arrays.sort(sortedBoundaryArray);
        }
        return sortedBoundaryArray;
    }

    private List getColumnBoundaryList()
    {
        return columnBoundaryList;
    }

    private void addColumnWidthToRowBoundaryMarker(final long width)
    {
        currentRowBoundaryMarker += width;
    }

    private long getCurrentRowBoundaryMarker()
    {
        return currentRowBoundaryMarker;
    }

    private void resetTableCounter()
    {
        tableCounter = 0;
    }

    private void resetCurrentRowBoundaryMarker()
    {
        currentRowBoundaryMarker = 0;
    }

    private ColumnBoundary[] getBoundariesForTable(final int table)
    {
        if (boundariesForTableArray == null)
        {
            final List boundariesForTable = new ArrayList();
            final List boundaryList = getColumnBoundaryList();
            for (int i = 0; i < boundaryList.size(); i++)
            {
                final ColumnBoundary b = (ColumnBoundary) boundaryList.get(i);
                if (b.isContainedByTable(table))
                {
                    boundariesForTable.add(b);
                }
            }
            boundariesForTableArray = (ColumnBoundary[]) boundariesForTable.toArray(new ColumnBoundary[boundariesForTable.size()]);
            Arrays.sort(boundariesForTableArray);
        }
        return boundariesForTableArray;
    }

    private int getColumnSpanForCell(final int table, final int col, final int initialColumnSpan)
    {
        final ColumnBoundary[] globalBoundaries = getSortedColumnBoundaryArray();
        final ColumnBoundary[] tableBoundaries = getBoundariesForTable(table);
        // how many column boundaries in the globalBoundaries list fall between the currentRowWidth and the next boundary
        // for the current row

        float cellBoundary = tableBoundaries[col - 1].getBoundary();
        float cellWidth = tableBoundaries[col - 1].getBoundary();

        if (col > 1)
        {
            cellWidth = cellWidth - tableBoundaries[col - 2].getBoundary();
        }

        if (initialColumnSpan > 1)
        {
            // ok we've got some additional spanning specified on the input
            final int index = (col - 1) + (initialColumnSpan - 1);
            cellWidth += tableBoundaries[index].getBoundary() - tableBoundaries[col - 1].getBoundary();
            cellBoundary = tableBoundaries[index].getBoundary();
        }

        int beginBoundaryIndex = 0;
        int endBoundaryIndex = globalBoundaries.length - 1;
        for (int i = 0; i < globalBoundaries.length; i++)
        {
            // find beginning boundary
            if (globalBoundaries[i].getBoundary() <= cellBoundary - cellWidth)
            {
                beginBoundaryIndex = i;
            }
            if (globalBoundaries[i].getBoundary() <= cellBoundary)
            {
                endBoundaryIndex = i;
            }
        }
        final int span = endBoundaryIndex - beginBoundaryIndex;
        // span will be zero for the first column, so we adjust it to 1
        if (span == 0)
        {
            return 1;
        }
        // System.out.println("table = " + table + " col = " + col + " rowBoundaries.length = " + tableBoundaries.length + "
        // cellWidth = " + cellWidth + " span = " + span);
        return span;
    }

    protected String getTargetMimeType()
    {
        return "application/vnd.oasis.opendocument.spreadsheet";
    }
}
