/* This file is part of the KDE project

   SPDX-FileCopyrightText: 2007 Johannes Simon <johannes.simon@gmail.com>
   SPDX-FileCopyrightText: 2009 Inge Wallin <inge@lysator.liu.se>

   SPDX-License-Identifier: LGPL-2.0-or-later
*/

// Own
#include "ChartProxyModel.h"
#include "PlotArea.h"

// Qt
#include <QPoint>
#include <QRegion>

// Calligra
#include <KoOdfGraphicStyles.h>
#include <KoOdfLoadingContext.h>
#include <KoOdfStylesReader.h>
#include <KoShapeLoadingContext.h>
#include <KoShapeSavingContext.h>
#include <KoStyleStack.h>
#include <KoXmlNS.h>
#include <KoXmlReader.h>
#include <interfaces/KoChartModel.h>

// KoChart
#include "Axis.h"
#include "ChartDebug.h"
#include "ChartTableModel.h"
#include "DataSet.h"
#include "OdfLoadingHelper.h"
#include "TableSource.h"

// Other
#include <algorithm>

using namespace KoChart;

// ================================================================
//                     Class ChartProxyModel::Private

class ChartProxyModel::Private
{
public:
    Private(ChartProxyModel *parent, ChartShape *shape, TableSource *source);
    ~Private();

    ChartProxyModel *const q;
    ChartShape *const shape;
    TableSource *const tableSource;

    /// Set to true if we're in the process of loading data from ODF.
    /// Used to avoid repeatedly updating data.
    bool isLoading;

    bool manualControl;

    bool firstRowIsLabel;
    bool firstColumnIsLabel;
    Qt::Orientation dataDirection;
    int dataDimensions;

    CellRegion categoryDataRegion;

    QVector<CellRegion> dataSetRegions;

    QList<DataSet *> dataSets;
    QList<DataSet *> removedDataSets;

    CellRegion selection;

    /**
     * Discards old and creates new data sets from the current region selection.
     */
    void rebuildDataMap();

    /**
     * Extracts a list of data sets (with x data region, y data region, etc.
     * assigned) from the current d->selection.
     *
     * Unless the list *dataSetsToRecycle is empty, it will reuse as many
     * DataSet instances from there as possible and remove them from the list.
     *
     * As a side effect, this method sets d->categoryDataRegion if
     * overrideCategories is true.
     */
    QList<DataSet *> createDataSetsFromRegion(QList<DataSet *> *dataSetsToRecycle, bool overrideCategories = true);
};

ChartProxyModel::Private::Private(ChartProxyModel *parent, ChartShape *shape, TableSource *source)
    : q(parent)
    , shape(shape)
    , tableSource(source)
    , isLoading(false)
    , manualControl(false)
{
    firstRowIsLabel = false;
    firstColumnIsLabel = false;
    dataDimensions = 1;

    // Determines what orientation the data points in a data series
    // have when multiple data sets are created from one source
    // region. For example, vertical means that each column in the source
    // region is assigned to one data series.
    // Default to Qt::Vertical, as that's what OOo does also.
    dataDirection = Qt::Vertical;
}

ChartProxyModel::Private::~Private()
{
    qDeleteAll(dataSets);
    qDeleteAll(removedDataSets);
}

// ================================================================
//                          Class ChartProxyModel

ChartProxyModel::ChartProxyModel(ChartShape *shape, TableSource *source)
    : QAbstractTableModel()
    , d(new Private(this, shape, source))
{
    connect(source, &TableSource::tableAdded, this, &ChartProxyModel::addTable);
    connect(source, &TableSource::tableRemoved, this, &ChartProxyModel::removeTable);
}

ChartProxyModel::~ChartProxyModel()
{
    delete d;
}

void ChartProxyModel::reset(const CellRegion &region)
{
    d->selection = region;
    d->rebuildDataMap();
}

CellRegion ChartProxyModel::cellRangeAddress() const
{
    return d->selection;
}

void ChartProxyModel::Private::rebuildDataMap()
{
    if (manualControl) {
        return;
    }
    // This was intended to speed up the loading process, by executing this
    // method only once in endLoading(), however the approach is actually
    // incorrect as it would potentially override a data set's regions
    // set by "somebody" else in the meantime.
    // if (isLoading)
    //     return;
    q->beginResetModel();
    q->invalidateDataSets();
    dataSets = createDataSetsFromRegion(&removedDataSets);
    q->endResetModel();
}

void ChartProxyModel::addTable(Table *table)
{
    QAbstractItemModel *model = table->model();

    connect(model, &QAbstractItemModel::dataChanged, this, QOverload<const QModelIndex &, const QModelIndex &>::of(&ChartProxyModel::dataChanged));
}

void ChartProxyModel::removeTable(Table *table)
{
    QAbstractItemModel *model = table->model();
    model->disconnect(this);
}

/**
 * Returns a row of a given region (i.e. a part of it with height 1), cutting
 * off the first @a colOffset cells in that row.
 *
 * Examples: extractRow(A1:C2, 0, 0) --> A1:C1
 *           extractRow(A1:C2, 1, 0) --> A2:C2
 *           extractRow(A1:C2, 0, 1) --> B1:C1
 *
 * See notes in createDataSetsFromRegion() for further details.
 *
 * @param region The region to extract the row from
 * @param row The number of the row, starting with 0
 * @param colOffset How many of the first columns to cut from the resulting row
 */
static QVector<QRect> extractRow(const QVector<QRect> &rects, int colOffset, bool extractLabel)
{
    if (colOffset == 0)
        return extractLabel ? QVector<QRect>() : rects;
    QVector<QRect> result;
    foreach (const QRect &rect, rects) {
        if (extractLabel) {
            QRect r(rect.topLeft(), QSize(colOffset, rect.height()));
            result.append(r);
        } else {
            // Q_ASSERT(rect.width() > colOffset);
            if (rect.width() > colOffset) {
                QPoint topLeft = rect.topLeft() + QPoint(colOffset, 0);
                QRect r(topLeft, QSize(rect.width() - colOffset, rect.height()));
                result.append(r);
            }
        }
    }
    return result;
}

/**
 * Returns a column of a given region, cutting off the first @a rowOffset
 * rows in that column.
 *
 * Examples: extractColumn(A1:C2, 0, 0)       --> A1:A2
 *           extractColumn(A1:C2;D1;F2, 0, 0) --> A1:A2;D1:D2
 *
 * See notes in createDataSetsFromRegion() for further details.
 *
 * @param region The region to extract the row from
 * @param col The number of the column, starting with 0
 * @param rowOffset How many of the first rows to cut from the resulting column
 */
static QVector<QRect> extractColumn(const QVector<QRect> &rects, int rowOffset, bool extractLabel)
{
    if (rowOffset == 0)
        return extractLabel ? QVector<QRect>() : rects;
    QVector<QRect> result;
    foreach (const QRect &rect, rects) {
        if (extractLabel) {
            QRect r(rect.topLeft(), QSize(rect.width(), rowOffset));
            result.append(r);
        } else {
            // Q_ASSERT(rect.height() > rowOffset);
            if (rect.height() > rowOffset) {
                QPoint topLeft = rect.topLeft() + QPoint(0, rowOffset);
                QRect r(topLeft, QSize(rect.width(), rect.height() - rowOffset));
                result.append(r);
            }
        }
    }
    return result;
}

/**
 * Returns a according to the dataDirection sorted data-region.
 *
 * First prepare the dataRegions to have them
 * - proper sorted either from left-to-right if the dataDirection is
 *   Qt::Horizontal or from top-to-bottom when Qt::Vertical.
 * - have multiple dimensions proper split-up into single rows/columns
 *   to produce one DataSet per row/column.
 *
 * The resulting map is build up depending on the dataDirection;
 * - Qt::Horizontal: key is the row and values are the columns in the row.
 * - Qt::Vertical: key is the column and values are the rows in the column.
 *
 * @param dataDirection The direction of the data. This could be either
 * dataDirection == Qt::Horizontal or dataDirection == Qt::Vertical.
 * @param dataRegions The unsorted list of data-regions.
 * @return Compared to the QVector that goes in the resulting map makes
 * following sure;
 * - sorted according to the dataDirection either by row or column
 * - duplicate definitions are removed/compressed.
 * - definitions or multiple rows and columns in one QRect are split
 *   in rows/columns what makes sure we earn one dataRegion per
 *   row/column. This is needed so for example the labelRegion and
 *   all other things operate on exactly one row/column.
 */
QMap<int, QVector<QRect>> sortDataRegions(Qt::Orientation dataDirection, QVector<QRect> dataRegions)
{
    QMap<int, QVector<QRect>> sortedDataRegions;
    if (dataDirection == Qt::Horizontal) {
        // Split up region in horizontal rectangles that are sorted from top to bottom
        QMap<int, QVector<QRect>> rows;
        foreach (const QRect &rect, dataRegions) {
            int x = rect.topLeft().x();
            for (int y = rect.topLeft().y(); y <= rect.bottomLeft().y(); y++) {
                QRect dataRect = QRect(QPoint(x, y), QSize(rect.width(), 1));
                if (!rows.contains(y))
                    rows.insert(y, QVector<QRect>());
                rows[y].append(dataRect);
            }
        }

        // Sort rectangles in each row from left to right.
        QMapIterator<int, QVector<QRect>> i(rows);
        while (i.hasNext()) {
            i.next();
            int row = i.key();
            QVector<QRect> unsortedRects = i.value();
            QVector<QRect> sortedRects;
            foreach (const QRect &rect, unsortedRects) {
                int index;
                for (index = 0; index < sortedRects.size(); index++)
                    if (rect.topLeft().x() <= sortedRects[index].topLeft().x())
                        break;
                sortedRects.insert(index, rect);
            }
            sortedDataRegions.insert(row, sortedRects);
        }
    } else {
        // Split up region in horizontal rectangles that are sorted from top to bottom
        QMap<int, QVector<QRect>> columns;
        foreach (const QRect &rect, dataRegions) {
            int y = rect.topLeft().y();
            for (int x = rect.topLeft().x(); x <= rect.topRight().x(); ++x) {
                QRect dataRect = QRect(QPoint(x, y), QSize(1, rect.height()));
                if (!columns.contains(x))
                    columns.insert(x, QVector<QRect>());
                columns[x].append(dataRect);
            }
        }

        // Sort rectangles in each column from top to bottom
        QMapIterator<int, QVector<QRect>> i(columns);
        while (i.hasNext()) {
            i.next();
            int col = i.key();
            QVector<QRect> unsortedRects = i.value();
            QVector<QRect> sortedRects;
            foreach (const QRect &rect, unsortedRects) {
                int index;
                for (index = 0; index < sortedRects.size(); ++index)
                    if (rect.topLeft().y() <= sortedRects[index].topLeft().y())
                        break;
                sortedRects.insert(index, rect);
            }
            sortedDataRegions.insert(col, sortedRects);
        }
    }
    return sortedDataRegions;
}

QList<DataSet *> ChartProxyModel::Private::createDataSetsFromRegion(QList<DataSet *> *dataSetsToRecycle, bool overrideCategories)
{
    if (manualControl) {
        return dataSets;
    }
    if (!selection.isValid())
        return QList<DataSet *>();

    // First prepare the dataRegions to have them
    // 1) proper sorted either from left-to-right if the dataDirection is
    //    Qt::Horizontal or from top-to-bottom when Qt::Vertical.
    // 2) have multiple dimensions proper split-up into single rows/columns
    //    to produce one DataSet per row/column.
    //
    // The resulting map is build up depending on the dataDirection;
    // - Qt::Horizontal: key is the row and values are the columns in the row.
    // - Qt::Vertical: key is the column and values are the rows in the column.
    QMap<int, QVector<QRect>> sortedDataRegions = sortDataRegions(dataDirection, selection.rects());

    // In the end, the contents of this list will look something like this:
    // (Category-Data, X-Data, Y-Data, Y-Data, Y-Data)
    // Semantic separation of the regions will follow later.
    QList<CellRegion> dataRegions;
    // This region exclusively contains (global) data set labels, i.e.
    // one label per data set (thus in opposite data direction)
    QList<CellRegion> labelRegions;

    // Fill dataRegions and set categoryRegion.
    // Note that here, we don't exactly know yet what region will be used for
    // what data set, we also don't know yet what data these regions contain.
    int rowOffset = firstRowIsLabel ? 1 : 0;
    int colOffset = firstColumnIsLabel ? 1 : 0;

    // Determines how many individual rows/columns will be assigned per data set.
    // It is at least one, but if there's more than one data dimension, the x
    // data is shared among all data sets, thus - 1.
    int regionsPerDataSet = qMax(1, dataDimensions - 1);

    // Determinate the number of rows and maximum number of columns used.
    // FIXME this logic is rather buggy. We better should use our sortedDataRegions
    //       here but that would need some testing for regression in e.g.
    //       bubble-charts which I don't had the time to do. So, leaving the logic as
    //       it was before for now. If you run into problems with bubble charts and
    //       there x-values then this foreach-logic here is very likely the reason.
    int rows = 0;
    int cols = 0;
    foreach (const QRect &rect, selection.rects()) {
        rows += rect.height();
        cols = qMax(cols, rect.width());
    }

    bool extractXData = dataDimensions > 1 &&
        // Checks if the remaining data regions would fit exactly to the
        // remaining data sets. If not, skip x data. This is only the case
        // for bubble charts, (the only case of regionsPerDataSet == 2), so
        // skipping x data will allow the last data set to also be assigned
        // a bubble width region. This is exactly what OOo does.
        (dataDirection == Qt::Horizontal ? rows - 1 - rowOffset : cols - 1 - colOffset) % regionsPerDataSet == 0;

    // When x data is present, it occupies the first non-header row/column
    if (extractXData && dataDirection == Qt::Horizontal)
        ++rowOffset;
    if (extractXData && dataDirection == Qt::Vertical)
        ++colOffset;

    // This is the logic that extracts all the subregions from selection
    // that are later used for the data sets
    Table *internalTable = shape ? shape->tableSource()->get(shape->internalModel()) : nullptr;
    QList<int> sortedDataKeys = sortedDataRegions.keys();
    std::sort(sortedDataKeys.begin(), sortedDataKeys.end());
    foreach (int key, sortedDataKeys) {
        QVector<QRect> rects = sortedDataRegions[key];
        QVector<QRect> dataRects;
        CellRegion labelRegion;
        if (dataDirection == Qt::Horizontal) {
            if (firstColumnIsLabel) {
                QVector<QRect> labelRects = extractRow(rects, colOffset, true);
                labelRegion = labelRects.isEmpty() ? CellRegion() : CellRegion(selection.table(), labelRects);
            } else {
                labelRegion = internalTable ? CellRegion(internalTable, QPoint(1, key)) : CellRegion();
            }
            dataRects = extractRow(rects, colOffset, false);
        } else {
            if (firstRowIsLabel) {
                QVector<QRect> labelRects = extractColumn(rects, rowOffset, true);
                labelRegion = labelRects.isEmpty() ? CellRegion() : CellRegion(selection.table(), labelRects);
            } else {
                labelRegion = internalTable ? CellRegion(internalTable, QPoint(key, 1)) : CellRegion();
            }
            dataRects = extractColumn(rects, rowOffset, false);
        }

        labelRegions.append(labelRegion);
        dataRegions.append(dataRects.isEmpty() ? CellRegion() : CellRegion(selection.table(), dataRects));
    }

    bool useCategories = (dataDirection == Qt::Horizontal && firstRowIsLabel) || (dataDirection == Qt::Vertical && firstColumnIsLabel);

    /*
    debugChart << "selection=" << selection.toString();
    debugChart << "dataDirection=" << (dataDirection == Qt::Horizontal ? "Horizontal" : "Vertical");
    debugChart << "firstRowIsLabel=" << firstRowIsLabel;
    debugChart << "firstColumnIsLabel=" << firstColumnIsLabel;
    debugChart << "overrideCategories=" << overrideCategories;
    debugChart << "useCategories=" << useCategories;
    debugChart << "dataRegions.count()="<<dataRegions.count();
    */

    // Regions shared by all data sets: categories and x-data

    // FIXME don't use overrideCategories for the categoryDataRegion's but for
    // the categoryLabelRegion (which we need to introduce and use).
    if (overrideCategories)
        categoryDataRegion = CellRegion();

    if (!dataRegions.isEmpty()) {
        // Q_ASSERT(label.isValid());
        // Q_ASSERT(data.isValid());
        if (useCategories) {
            labelRegions.takeFirst();
            categoryDataRegion = dataRegions.takeFirst();
        }
    }

    CellRegion xData;
    if (!dataRegions.isEmpty() && extractXData) {
        labelRegions.removeFirst();
        xData = dataRegions.takeFirst();
    }

    QList<DataSet *> createdDataSets;
    int dataSetNumber = 0;
    // Now assign all dataRegions to a number of data sets.
    // Here they're semantically separated into x data, y data, etc.
    Q_ASSERT(dataRegions.count() == labelRegions.count());
    while (!dataRegions.isEmpty()) {
        // Get a data set instance we can use
        DataSet *dataSet;
        if (!dataSetsToRecycle->isEmpty())
            dataSet = dataSetsToRecycle->takeFirst();
        else
            dataSet = new DataSet(dataSetNumber);

        // category and x data are "global" regions shared among all data sets
        dataSet->setCategoryDataRegion(categoryDataRegion);
        dataSet->setXDataRegion(xData);

        // the name-value used for e.g. the legend label
        dataSet->setLabelDataRegion(labelRegions.takeFirst());

        // the range for y-values
        dataSet->setYDataRegion(dataRegions.takeFirst());

        // the custom data (e.g. bubble width)
        if (!dataRegions.isEmpty() && dataDimensions > 2)
            dataSet->setCustomDataRegion(dataRegions.takeFirst());
        else
            dataSet->setCustomDataRegion(CellRegion());

        /*
        debugChart << "xDataRegion=" << dataSet->xDataRegion().toString();
        debugChart << "yDataRegion=" << dataSet->yDataRegion().toString();
        debugChart << "categoryDataRegion=" << dataSet->categoryDataRegion().toString();
        debugChart << "labelDataRegion=" << dataSet->labelDataRegion().toString();
        debugChart << "customDataRegion=" << dataSet->customDataRegion().toString();
        */

        createdDataSets.append(dataSet);

        // Increment number at the very end!
        dataSetNumber++;
    }
    return createdDataSets;
}

void ChartProxyModel::saveOdf(KoShapeSavingContext &context) const
{
    int dataSetCount = rowCount();
    if (d->shape->chartType() == StockChartType && d->shape->chartSubType() == HighLowCloseChartSubtype && dataSetCount > 3) {
        // When loading, stock chart subtype is determined by number of datasets (sigh)
        dataSetCount = 3;
    }
    for (int i = 0; i < dataSetCount; ++i) {
        d->dataSets.at(i)->saveOdf(context);
    }
}

// This loads the properties of the datasets (chart:series).
// FIXME: This is a strange place to load them (the proxy model)
bool ChartProxyModel::loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context, ChartType type)
{
    Q_ASSERT(d->isLoading);

    //     PlotArea* plotArea = dynamic_cast< PlotArea* >(QObject::parent());

    //     const bool stockChart = element.attributeNS(KoXmlNS::chart, "class", QString()) == "chart:stock";

    OdfLoadingHelper *helper = (OdfLoadingHelper *)context.sharedData(OdfLoadingHelperId);
    bool ignoreCellRanges = helper->chartUsesInternalModelOnly;
// Some OOo documents save incorrect cell ranges. For those this fix was intended.
// Find out which documents exactly and only use fix for as few cases as possible.
#if 0
    // If we exclusively use the chart's internal model then all data
    // is taken from there and each data set is automatically assigned
    // the rows it belongs to.
    bool ignoreCellRanges = helper->chartUsesInternalModelOnly;
#endif

    beginResetModel();

    if (element.hasAttributeNS(KoXmlNS::chart, "style-name")) {
        KoStyleStack &styleStack = context.odfLoadingContext().styleStack();
        styleStack.clear();
        context.odfLoadingContext().fillStyleStack(element, KoXmlNS::chart, "style-name", "chart");

        // Data direction: It's in the plotarea style.
        if (styleStack.hasProperty(KoXmlNS::chart, "series-source")) {
            QString seriesSource = styleStack.property(KoXmlNS::chart, "series-source");
            // Check if the direction for data series is vertical or horizontal.
            if (seriesSource == "rows")
                d->dataDirection = Qt::Horizontal;
            else if (seriesSource == "columns")
                d->dataDirection = Qt::Vertical;
            // Otherwise leave our default value
        }
    }

    // Find out if the data table contains labels as first row and/or column.
    // This is in the plot-area element itself.

    // Do not ignore the data-source-has-labels in any case, even if a
    // category data region is specified for an axis, as the first
    // column still has to be excluded from the actual data region if
    // e.g. data-source-has-labels is set to "column" If an axis
    // contains the chart:categories element, the category data region
    // will automatically be set on every data set attached to that
    // axis. See Axis::attachDataSet().
    const QString dataSourceHasLabels = element.attributeNS(KoXmlNS::chart, "data-source-has-labels");
    if (dataSourceHasLabels == "both") {
        d->firstRowIsLabel = true;
        d->firstColumnIsLabel = true;
    } else if (dataSourceHasLabels == "row") {
        d->firstRowIsLabel = true;
        d->firstColumnIsLabel = false;
    } else if (dataSourceHasLabels == "column") {
        d->firstRowIsLabel = false;
        d->firstColumnIsLabel = true;
    } else {
        // dataSourceHasLabels == "none" or wrong value
        d->firstRowIsLabel = false;
        d->firstColumnIsLabel = false;
    }

    // For every dataset, there must be an explicit <chart:series> element,
    // which we will load later.
    d->dataSets.clear();
    d->removedDataSets.clear();

    // A cell range for all data is optional.
    // If cell ranges are in addition specified for one or more of these
    // data series, they'll be overwritten by the data loaded from the series
    if (!ignoreCellRanges && element.hasAttributeNS(KoXmlNS::table, "cell-range-address")) {
        QString cellRangeAddress = element.attributeNS(KoXmlNS::table, "cell-range-address");
        d->selection = CellRegion(d->tableSource, cellRangeAddress);
        // Otherwise use all data from internal table
    } else if (helper->chartUsesInternalModelOnly) {
        QList<Table *> tables = helper->tableSource->tableMap().values();
        Q_ASSERT(tables.count() == 1);
        Table *internalTable = tables.first();
        Q_ASSERT(internalTable->model());
        int rowCount = internalTable->model()->rowCount();
        int colCount = internalTable->model()->columnCount();
        d->selection = CellRegion(internalTable, QRect(1, 1, colCount, rowCount));
    }

    // This is what we'll use as basis for the data sets we "produce" from ODF.
    // This might be data sets that were "instantiated" from the internal
    // table or from an arbitrary selection of other tables as specified
    // in the PlotArea's table:cell-range-address attribute (parsed above).
    QList<DataSet *> createdDataSets = d->createDataSetsFromRegion(&d->removedDataSets, !helper->categoryRegionSpecifiedInXAxis);

    bool isBubble = d->shape->plotArea()->chartType() == BubbleChartType;
    bool isScatter = d->shape->plotArea()->chartType() == ScatterChartType;
    CellRegion prevXData, prevYData;

    int loadedDataSetCount = 0;
    KoXmlElement n;
    forEachElement(n, element)
    {
        if (n.namespaceURI() != KoXmlNS::chart) {
            continue;
        }
        if (n.localName() == "series") {
            DataSet *dataSet;
            if (loadedDataSetCount < createdDataSets.size()) {
                dataSet = createdDataSets[loadedDataSetCount];
            } else {
                // the datasetnumber needs to be known at construction time, to ensure
                // default colors are set correctly
                dataSet = new DataSet(d->dataSets.size());
            }
            dataSet->setChartType(type);
            d->dataSets.append(dataSet);
            if (d->categoryDataRegion.isValid()) {
                dataSet->setCategoryDataRegion(d->categoryDataRegion);
            }
            dataSet->loadOdf(n, context);

            if (isBubble || isScatter) {
                // bubble- and scatter-charts have chart:domain's that define the
                // x- and y-data. But if they are not defined in the series then
                // a previous defined one needs to be used.
                // NOTE: This is not quite what odf specifies, but imho better (danders)
                if (dataSet->xDataRegion().isValid()) {
                    prevXData = dataSet->xDataRegion();
                } else {
                    dataSet->setXDataRegion(prevXData);
                }
                if (isBubble) {
                    if (dataSet->yDataRegion().isValid()) {
                        prevYData = dataSet->yDataRegion();
                    } else {
                        dataSet->setYDataRegion(prevYData);
                    }
                }
            }
            ++loadedDataSetCount;
        }
    }

    // rebuildDataMap();
    endResetModel();

    return true;
}

QVariant ChartProxyModel::data(const QModelIndex &index, int role) const
{
    Q_UNUSED(index);
    Q_UNUSED(role);
    Q_ASSERT("To be implemented");
    return QVariant();
}

void ChartProxyModel::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
{
    QPoint topLeftPoint(topLeft.column() + 1, topLeft.row() + 1);

    // Excerpt from the Qt reference for QRect::bottomRight() which is
    // used for calculating bottomRight.  Note that for historical
    // reasons this function returns
    //   QPoint(left() + width() -1, top() + height() - 1).
    QPoint bottomRightPoint(bottomRight.column() + 1, bottomRight.row() + 1);
    QRect dataChangedRect = QRect(topLeftPoint, QSize(bottomRightPoint.x() - topLeftPoint.x() + 1, bottomRightPoint.y() - topLeftPoint.y() + 1));
    // Precisely determine what data in what table changed so that we don't
    // do unnecessary, expensive updates.
    Table *table = d->tableSource->get(topLeft.model());
    CellRegion dataChangedRegion(table, dataChangedRect);

    foreach (DataSet *dataSet, d->dataSets) {
        if (dataSet->xDataRegion().intersects(dataChangedRegion))
            dataSet->xDataChanged(QRect());

        if (dataSet->yDataRegion().intersects(dataChangedRegion))
            dataSet->yDataChanged(QRect());

        if (dataSet->categoryDataRegion().intersects(dataChangedRegion))
            dataSet->categoryDataChanged(QRect());

        if (dataSet->labelDataRegion().intersects(dataChangedRegion))
            dataSet->labelDataChanged(QRect());

        if (dataSet->customDataRegion().intersects(dataChangedRegion))
            dataSet->customDataChanged(QRect());
    }

    Q_EMIT dataChanged();
}

QVariant ChartProxyModel::headerData(int section, Qt::Orientation orientation, int role /* = Qt::DisplayRole */) const
{
    Q_UNUSED(section);
    Q_UNUSED(orientation);
    Q_UNUSED(role);
    Q_ASSERT("To be implemented");
    return QVariant();
}

QModelIndex ChartProxyModel::parent(const QModelIndex &index) const
{
    Q_UNUSED(index);

    return QModelIndex();
}

int ChartProxyModel::rowCount(const QModelIndex & /*parent  = QModelIndex() */) const
{
    return d->dataSets.count();
}

int ChartProxyModel::columnCount(const QModelIndex & /*parent = QModelIndex() */) const
{
    // FIXME: Replace this by the actual column count once the proxy is properly being used.
    int cols = 0;
    for (int i = 0; i < d->dataSets.count(); ++i) {
        cols = qMax(cols, d->dataSets.at(i)->size());
    }
    return cols;
}

void ChartProxyModel::setFirstRowIsLabel(bool b)
{
    if (b == d->firstRowIsLabel)
        return;

    d->firstRowIsLabel = b;
    d->rebuildDataMap();
}

void ChartProxyModel::setFirstColumnIsLabel(bool b)
{
    if (b == d->firstColumnIsLabel)
        return;

    d->firstColumnIsLabel = b;
    d->rebuildDataMap();
}

Qt::Orientation ChartProxyModel::dataDirection()
{
    return d->dataDirection;
}

void ChartProxyModel::invalidateDataSets()
{
    d->removedDataSets = d->dataSets;
    d->dataSets.clear();
}

void ChartProxyModel::beginLoading()
{
    Q_ASSERT(!d->isLoading);

    beginResetModel();

    // FIXME: invalidateDataSets() used to be called explicitly at the beginning
    // of ChartShape::loadOdf(). Now beginLoading() is called instead.
    // So, is invalidateDataSets() still necessary here?
    invalidateDataSets();
    d->isLoading = true;
}

void ChartProxyModel::endLoading()
{
    Q_ASSERT(d->isLoading);
    d->isLoading = false;

    // Doing this here is wrong, the data set's cell regions set in
    // DataSet::loadOdf() would get overridden.
    // d->rebuildDataMap();

    endResetModel();
}

bool ChartProxyModel::isLoading() const
{
    return d->isLoading;
}

void ChartProxyModel::setDataDirection(Qt::Orientation orientation)
{
    if (d->dataDirection == orientation)
        return;

    d->dataDirection = orientation;
    d->rebuildDataMap();
}

void ChartProxyModel::setDataDimensions(int dimensions)
{
    if (d->dataDimensions == dimensions)
        return;

    d->dataDimensions = dimensions;
    d->rebuildDataMap();
}

bool ChartProxyModel::firstRowIsLabel() const
{
    return d->firstRowIsLabel;
}

bool ChartProxyModel::firstColumnIsLabel() const
{
    return d->firstColumnIsLabel;
}

CellRegion ChartProxyModel::categoryDataRegion() const
{
    return d->categoryDataRegion;
}

void ChartProxyModel::setCategoryDataRegion(const CellRegion &region)
{
    d->categoryDataRegion = region;
}

QList<DataSet *> ChartProxyModel::dataSets() const
{
    return d->dataSets;
}

void ChartProxyModel::addDataSet(int pos)
{
    int dataSetNr = pos;
    QMap<int, int> numbers;
    for (int i = 0; i < d->dataSets.count(); ++i) {
        numbers[d->dataSets.at(i)->number()] = i;
    }
    if (numbers.contains(pos)) {
        // find first free one
        for (int i = 1; i < numbers.count(); ++i) {
            if (!numbers.contains(i)) {
                dataSetNr = i;
                break;
            }
        }
    }
    DataSet *ds = new DataSet(dataSetNr);
    if (!d->dataSets.isEmpty()) {
        DataSet *copy = d->dataSets.first();
        ds->setXDataRegion(copy->xDataRegion());
        ds->setYDataRegion(copy->yDataRegion());
        ds->setCustomDataRegion(copy->customDataRegion());
        ds->setCategoryDataRegion(copy->categoryDataRegion());
    }
    d->dataSets.insert(pos, ds);
}

bool ChartProxyModel::insertRows(int row, int count, const QModelIndex & /*parent*/)
{
    if (count < 1 || row < 0 || row > d->dataSets.count()) {
        return false;
    }
    beginResetModel();
    for (int i = 0; i < count; ++i) {
        addDataSet(row + i);
    }
    endResetModel();
    dataChanged(QModelIndex(), QModelIndex());
    return true;
}

bool ChartProxyModel::removeRows(int row, int count, const QModelIndex & /*parent*/)
{
    if (count < 1 || row < 0 || row >= d->dataSets.count()) {
        return false;
    }
    beginResetModel();
    QList<DataSet *> remove;
    for (int i = 0; i < count; ++i) {
        remove << d->dataSets.at(row + i);
    }
    for (DataSet *ds : remove) {
        d->dataSets.removeAll(ds);
        delete ds;
    }
    endResetModel();
    dataChanged(QModelIndex(), QModelIndex());
    return true;
}

void ChartProxyModel::setManualControl(bool value)
{
    d->manualControl = value;
}

bool ChartProxyModel::manualControl()
{
    return d->manualControl;
}
