// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <private/areachartitem_p.h>
#include <QtCharts/QAreaSeries>
#include <private/qareaseries_p.h>
#include <QtCharts/QLineSeries>
#include <private/chartpresenter_p.h>
#include <private/abstractdomain_p.h>
#include <private/chartdataset_p.h>
#include <QtGui/QPainter>
#include <QtWidgets/QGraphicsSceneMouseEvent>
#include <QtCore/QDebug>


QT_BEGIN_NAMESPACE

AreaChartItem::AreaChartItem(QAreaSeries *areaSeries, QGraphicsItem* item)
    : ChartItem(areaSeries->d_func(),item),
      m_series(areaSeries),
      m_upper(0),
      m_lower(0),
      m_pointsVisible(false),
      m_pointLabelsVisible(false),
      m_pointLabelsFormat(areaSeries->pointLabelsFormat()),
      m_pointLabelsFont(areaSeries->pointLabelsFont()),
      m_pointLabelsColor(areaSeries->pointLabelsColor()),
      m_pointLabelsClipping(true),
      m_mousePressed(false)
{
    setAcceptHoverEvents(true);
    setFlag(QGraphicsItem::ItemIsSelectable, true);
    setZValue(ChartPresenter::LineChartZValue);
    if (m_series->upperSeries())
        m_upper = new AreaBoundItem(this, m_series->upperSeries());
    if (m_series->lowerSeries())
        m_lower = new AreaBoundItem(this, m_series->lowerSeries());

    QObject::connect(m_series->d_func(), SIGNAL(updated()), this, SLOT(handleUpdated()));
    QObject::connect(m_series, SIGNAL(visibleChanged()), this, SLOT(handleUpdated()));
    QObject::connect(m_series, SIGNAL(opacityChanged()), this, SLOT(handleUpdated()));
    QObject::connect(this, SIGNAL(clicked(QPointF)), areaSeries, SIGNAL(clicked(QPointF)));
    QObject::connect(this, SIGNAL(hovered(QPointF,bool)), areaSeries, SIGNAL(hovered(QPointF,bool)));
    QObject::connect(this, SIGNAL(pressed(QPointF)), areaSeries, SIGNAL(pressed(QPointF)));
    QObject::connect(this, SIGNAL(released(QPointF)), areaSeries, SIGNAL(released(QPointF)));
    QObject::connect(this, SIGNAL(doubleClicked(QPointF)),
                     areaSeries, SIGNAL(doubleClicked(QPointF)));
    QObject::connect(areaSeries, SIGNAL(pointLabelsFormatChanged(QString)),
                     this, SLOT(handleUpdated()));
    QObject::connect(areaSeries, SIGNAL(pointLabelsVisibilityChanged(bool)),
                     this, SLOT(handleUpdated()));
    QObject::connect(areaSeries, SIGNAL(pointLabelsFontChanged(QFont)),
                     this, SLOT(handleUpdated()));
    QObject::connect(areaSeries, SIGNAL(pointLabelsColorChanged(QColor)),
                     this, SLOT(handleUpdated()));
    QObject::connect(areaSeries, SIGNAL(pointLabelsClippingChanged(bool)),
                     this, SLOT(handleUpdated()));

    handleUpdated();
}

AreaChartItem::~AreaChartItem()
{
    delete m_upper;
    delete m_lower;
}

void AreaChartItem::setPresenter(ChartPresenter *presenter)
{
    if (m_upper)
        m_upper->setPresenter(presenter);
    if (m_lower)
        m_lower->setPresenter(presenter);
    ChartItem::setPresenter(presenter);
}

void AreaChartItem::setUpperSeries(QLineSeries *series)
{
    delete m_upper;
    if (series)
        m_upper = new AreaBoundItem(this, series);
    else
        m_upper = 0;
    if (m_upper) {
        m_upper->setPresenter(presenter());
        fixEdgeSeriesDomain(m_upper);
    } else {
        updatePath();
    }
}

void AreaChartItem::setLowerSeries(QLineSeries *series)
{
    delete m_lower;
    if (series)
        m_lower = new AreaBoundItem(this, series);
    else
        m_lower = 0;
    if (m_lower) {
        m_lower->setPresenter(presenter());
        fixEdgeSeriesDomain(m_lower);
    } else {
        updatePath();
    }
}

QRectF AreaChartItem::boundingRect() const
{
    return m_rect;
}

QPainterPath AreaChartItem::shape() const
{
    return m_path;
}

void AreaChartItem::updatePath()
{
    QPainterPath path;
    QRectF rect(QPointF(0,0),domain()->size());

    if (m_upper) {
        path = m_upper->path();

        if (m_lower) {
            // Note: Polarcharts draw area correctly only when both series have equal width or are
            // fully displayed. If one series is partally off-chart, the connecting line between
            // the series does not attach to the end of the partially hidden series but to the point
            // where it intersects the axis line. The problem is especially noticeable when one of
            // the series is entirely off-chart, in which case the connecting line connects two
            // ends of the visible series.
            // This happens because we get the paths from linechart, which omits off-chart segments.
            // To properly fix, linechart would need to provide true full path, in right, left,
            // and the rest portions to enable proper clipping. However, combining those to single
            // visually unified area would be a nightmare, since they would have to be painted
            // separately.
            path.connectPath(m_lower->path().toReversed());
        } else {
            QPointF first = path.pointAtPercent(0);
            QPointF last =  path.pointAtPercent(1);
            if (presenter()->chartType() == QChart::ChartTypeCartesian) {
                path.lineTo(last.x(), rect.bottom());
                path.lineTo(first.x(), rect.bottom());
            } else { // polar
                path.lineTo(rect.center());
            }
        }
        path.closeSubpath();
    }

    // Only zoom in if the bounding rect of the path fits inside int limits. QWidget::update() uses
    // a region that has to be compatible with QRect.
    if (path.boundingRect().height() <= INT_MAX
            && path.boundingRect().width() <= INT_MAX) {
        prepareGeometryChange();
        m_path = path;
        m_rect = path.boundingRect();
        update();
    }
}

void AreaChartItem::handleUpdated()
{
    setVisible(m_series->isVisible());
    m_pointsVisible = m_series->pointsVisible();
    m_linePen = m_series->pen();
    m_brush = m_series->brush();
    m_pointPen = m_series->pen();
    m_pointPen.setWidthF(2 * m_pointPen.width());
    setOpacity(m_series->opacity());
    m_pointLabelsFormat = m_series->pointLabelsFormat();
    m_pointLabelsVisible = m_series->pointLabelsVisible();
    m_pointLabelsFont = m_series->pointLabelsFont();
    m_pointLabelsColor = m_series->pointLabelsColor();
    bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping();
    m_pointLabelsClipping = m_series->pointLabelsClipping();
    // Update whole chart in case label clipping changed as labels can be outside series area
    if (labelClippingChanged)
        m_series->chart()->update();
    else
        update();
}

void AreaChartItem::handleDomainUpdated()
{
    fixEdgeSeriesDomain(m_upper);
    fixEdgeSeriesDomain(m_lower);
}

void AreaChartItem::fixEdgeSeriesDomain(LineChartItem *edgeSeries)
{
    if (edgeSeries) {
        AbstractDomain* mainDomain = domain();
        AbstractDomain* edgeDomain = edgeSeries->domain();

        if (edgeDomain->type() != mainDomain->type()) {
            // Change the domain of edge series to the same type as the area series
            edgeDomain = dataSet()->createDomain(mainDomain->type());
            edgeSeries->seriesPrivate()->setDomain(edgeDomain);
        }
        edgeDomain->setSize(mainDomain->size());
        edgeDomain->setRange(mainDomain->minX(), mainDomain->maxX(), mainDomain->minY(), mainDomain->maxY());
        edgeDomain->setReverseX(mainDomain->isReverseX());
        edgeDomain->setReverseY(mainDomain->isReverseY());
        edgeSeries->handleDomainUpdated();
    }
}

void AreaChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(widget);
    Q_UNUSED(option);

    painter->save();
    painter->setPen(m_linePen);
    painter->setBrush(m_brush);
    QRectF clipRect = QRectF(QPointF(0, 0), domain()->size());
    if (presenter()->chartType() == QChart::ChartTypePolar)
        painter->setClipRegion(QRegion(clipRect.toRect(), QRegion::Ellipse));
    else
        painter->setClipRect(clipRect);

    painter->drawPath(m_path);
    if (m_pointsVisible) {
        painter->setPen(m_pointPen);
        if (m_upper)
            painter->drawPoints(m_upper->geometryPoints());
        if (m_lower)
            painter->drawPoints(m_lower->geometryPoints());
    }

    // Draw series point label
    if (m_pointLabelsVisible) {
        static const QString xPointTag(QLatin1String("@xPoint"));
        static const QString yPointTag(QLatin1String("@yPoint"));
        const int labelOffset = 2;

        if (m_pointLabelsClipping)
            painter->setClipping(true);
        else
            painter->setClipping(false);

        QFont f(m_pointLabelsFont);
        f.setPixelSize(QFontInfo(m_pointLabelsFont).pixelSize());
        painter->setFont(f);
        painter->setPen(QPen(m_pointLabelsColor));
        QFontMetrics fm(painter->font());

        QString pointLabel;

        if (m_series->upperSeries()) {
            for (int i(0); i < m_series->upperSeries()->count(); i++) {
                pointLabel = m_pointLabelsFormat;
                pointLabel.replace(xPointTag,
                                   presenter()->numberToString(m_series->upperSeries()->at(i).x()));
                pointLabel.replace(yPointTag,
                                   presenter()->numberToString(m_series->upperSeries()->at(i).y()));

                // Position text in relation to the point
                int pointLabelWidth = fm.horizontalAdvance(pointLabel);
                QPointF position(m_upper->geometryPoints().at(i));
                position.setX(position.x() - pointLabelWidth / 2);
                position.setY(position.y() - m_series->upperSeries()->pen().width() / 2
                              - labelOffset);
                painter->drawText(position, pointLabel);
            }
        }

        if (m_series->lowerSeries()) {
            for (int i(0); i < m_series->lowerSeries()->count(); i++) {
                pointLabel = m_pointLabelsFormat;
                pointLabel.replace(xPointTag,
                                   presenter()->numberToString(m_series->lowerSeries()->at(i).x()));
                pointLabel.replace(yPointTag,
                                   presenter()->numberToString(m_series->lowerSeries()->at(i).y()));

                // Position text in relation to the point
                int pointLabelWidth = fm.horizontalAdvance(pointLabel);
                QPointF position(m_lower->geometryPoints().at(i));
                position.setX(position.x() - pointLabelWidth / 2);
                position.setY(position.y() - m_series->lowerSeries()->pen().width() / 2
                              - labelOffset);
                painter->drawText(position, pointLabel);
            }
        }
    }

    painter->restore();
}

void AreaChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    emit pressed(domain()->calculateDomainPoint(event->pos()));
    m_lastMousePos = event->pos();
    m_mousePressed = true;
    ChartItem::mousePressEvent(event);
}

void AreaChartItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
    emit hovered(domain()->calculateDomainPoint(event->pos()), true);
    event->accept();
//    QGraphicsItem::hoverEnterEvent(event);
}

void AreaChartItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
{
    emit hovered(domain()->calculateDomainPoint(event->pos()), false);
    event->accept();
//    QGraphicsItem::hoverEnterEvent(event);
}

void AreaChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    emit released(domain()->calculateDomainPoint(m_lastMousePos));
    if (m_mousePressed)
        emit clicked(domain()->calculateDomainPoint(m_lastMousePos));
    m_mousePressed = false;
    ChartItem::mouseReleaseEvent(event);
}

void AreaChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
    emit doubleClicked(domain()->calculateDomainPoint(m_lastMousePos));
    ChartItem::mouseDoubleClickEvent(event);
}

QT_END_NAMESPACE

#include "moc_areachartitem_p.cpp"
