/*
 * SPDX-FileCopyrightText: 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved.
 *
 * This file is part of the KD Chart library.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "KChartRingDiagram.h"
#include "KChartRingDiagram_p.h"

#include "KChartAttributesModel.h"
#include "KChartDataValueAttributes.h"
#include "KChartMath_p.h"
#include "KChartPaintContext.h"
#include "KChartPainterSaver_p.h"
#include "KChartPieAttributes.h"
#include "KChartPolarCoordinatePlane_p.h"
#include "KChartThreeDPieAttributes.h"

#include <QPainter>

using namespace KChart;

RingDiagram::Private::Private()
    : relativeThickness(false)
    , expandWhenExploded(false)
{
}

RingDiagram::Private::~Private()
{
}

#define d d_func()

RingDiagram::RingDiagram(QWidget *parent, PolarCoordinatePlane *plane)
    : AbstractPieDiagram(new Private(), parent, plane)
{
    init();
}

RingDiagram::~RingDiagram()
{
}

void RingDiagram::init()
{
}

RingDiagram *RingDiagram::clone() const
{
    return new RingDiagram(new Private(*d));
}

bool RingDiagram::compare(const RingDiagram *other) const
{
    if (other == this)
        return true;
    if (!other) {
        return false;
    }
    return // compare the base class
        (static_cast<const AbstractPieDiagram *>(this)->compare(other)) &&
        // compare own properties
        (relativeThickness() == other->relativeThickness()) && (expandWhenExploded() == other->expandWhenExploded());
}

void RingDiagram::setRelativeThickness(bool relativeThickness)
{
    d->relativeThickness = relativeThickness;
}

bool RingDiagram::relativeThickness() const
{
    return d->relativeThickness;
}

void RingDiagram::setExpandWhenExploded(bool expand)
{
    d->expandWhenExploded = expand;
}

bool RingDiagram::expandWhenExploded() const
{
    return d->expandWhenExploded;
}

const QPair<QPointF, QPointF> RingDiagram::calculateDataBoundaries() const
{
    if (!checkInvariants(true))
        return QPair<QPointF, QPointF>(QPointF(0, 0), QPointF(0, 0));

    const PieAttributes attrs(pieAttributes());

    QPointF bottomLeft(0, 0);
    QPointF topRight;
    // If we explode, we need extra space for the pie slice that has the largest explosion distance.
    if (attrs.explode()) {
        const int rCount = rowCount();
        const int colCount = columnCount();
        qreal maxExplode = 0.0;
        for (int i = 0; i < rCount; ++i) {
            qreal maxExplodeInThisRow = 0.0;
            for (int j = 0; j < colCount; ++j) {
                const PieAttributes columnAttrs(pieAttributes(model()->index(i, j, rootIndex()))); // checked
                maxExplodeInThisRow = qMax(maxExplodeInThisRow, columnAttrs.explodeFactor());
            }
            maxExplode += maxExplodeInThisRow;

            // FIXME: What if explode factor of inner ring is > 1.0 ?
            if (!d->expandWhenExploded) {
                break;
            }
        }
        // explode factor is relative to width (outer r - inner r) of one ring
        maxExplode /= (rCount + 1);
        topRight = QPointF(1.0 + maxExplode, 1.0 + maxExplode);
    } else {
        topRight = QPointF(1.0, 1.0);
    }
    return QPair<QPointF, QPointF>(bottomLeft, topRight);
}

void RingDiagram::paintEvent(QPaintEvent *)
{
    QPainter painter(viewport());
    PaintContext ctx;
    ctx.setPainter(&painter);
    ctx.setRectangle(QRectF(0, 0, width(), height()));
    paint(&ctx);
}

void RingDiagram::resizeEvent(QResizeEvent *)
{
}

void RingDiagram::paint(PaintContext *ctx)
{
    // note: Not having any data model assigned is no bug
    //       but we can not draw a diagram then either.
    if (!checkInvariants(true))
        return;

    d->reverseMapper.clear();

    const PieAttributes attrs(pieAttributes());

    const int rCount = rowCount();
    const int colCount = columnCount();

    // QRectF contentsRect = PolarCoordinatePlane::Private::contentsRect( polarCoordinatePlane() );
    QRectF contentsRect = ctx->rectangle();
    if (contentsRect.isEmpty())
        return;

    d->startAngles = QVector<QVector<qreal>>(rCount, QVector<qreal>(colCount));
    d->angleLens = QVector<QVector<qreal>>(rCount, QVector<qreal>(colCount));

    // compute position
    d->size = qMin(contentsRect.width(), contentsRect.height()); // initial size

    // if the slices explode, we need to give them additional space =>
    // make the basic size smaller
    qreal totalOffset = 0.0;
    for (int i = 0; i < rCount; ++i) {
        qreal maxOffsetInThisRow = 0.0;
        for (int j = 0; j < colCount; ++j) {
            const PieAttributes cellAttrs(pieAttributes(model()->index(i, j, rootIndex()))); // checked
            // qDebug() << cellAttrs.explodeFactor();
            const qreal explode = cellAttrs.explode() ? cellAttrs.explodeFactor() : 0.0;
            maxOffsetInThisRow = qMax(maxOffsetInThisRow, cellAttrs.gapFactor(false) + explode);
        }
        if (!d->expandWhenExploded) {
            maxOffsetInThisRow -= qreal(i);
        }
        totalOffset += qMax<qreal>(maxOffsetInThisRow, 0.0);
        // FIXME: What if explode factor of inner ring is > 1.0 ?
        // if ( !d->expandWhenExploded )
        //      break;
    }

    // explode factor is relative to width (outer r - inner r) of one ring
    if (rCount > 0)
        totalOffset /= (rCount + 1);
    d->size /= (1.0 + totalOffset);

    qreal x = (contentsRect.width() == d->size) ? 0.0 : ((contentsRect.width() - d->size) / 2.0);
    qreal y = (contentsRect.height() == d->size) ? 0.0 : ((contentsRect.height() - d->size) / 2.0);
    d->position = QRectF(x, y, d->size, d->size);
    d->position.translate(contentsRect.left(), contentsRect.top());

    const PolarCoordinatePlane *plane = polarCoordinatePlane();

    d->forgetAlreadyPaintedDataValues();
    for (int iRow = 0; iRow < rCount; ++iRow) {
        const qreal sum = valueTotals(iRow);
        if (sum == 0.0) // nothing to draw
            continue;
        qreal currentValue = plane ? plane->startPosition() : 0.0;
        const qreal sectorsPerValue = 360.0 / sum;

        for (int iColumn = 0; iColumn < colCount; ++iColumn) {
            // is there anything at all at this column?
            bool bOK;
            const qreal cellValue = qAbs(model()
                                             ->data(model()->index(iRow, iColumn, rootIndex())) // checked
                                             .toReal(&bOK));

            if (bOK) {
                d->startAngles[iRow][iColumn] = currentValue;
                d->angleLens[iRow][iColumn] = cellValue * sectorsPerValue;
            } else { // mark as non-existent
                d->angleLens[iRow][iColumn] = 0.0;
                if (iColumn > 0.0) {
                    d->startAngles[iRow][iColumn] = d->startAngles[iRow][iColumn - 1];
                } else {
                    d->startAngles[iRow][iColumn] = currentValue;
                }
            }

            currentValue = d->startAngles[iRow][iColumn] + d->angleLens[iRow][iColumn];

            drawOneSlice(ctx->painter(), iRow, iColumn, granularity());
        }
    }
}

#if defined(Q_WS_WIN)
#define trunc(x) ((int)(x))
#endif

void RingDiagram::drawOneSlice(QPainter *painter, uint dataset, uint slice, qreal granularity)
{
    // Is there anything to draw at all?
    const qreal angleLen = d->angleLens[dataset][slice];
    if (angleLen) {
        drawPieSurface(painter, dataset, slice, granularity);
    }
}

void RingDiagram::resize(const QSizeF &size)
{
    AbstractPieDiagram ::resize(size);
}

void RingDiagram::drawPieSurface(QPainter *painter, uint dataset, uint slice, qreal granularity)
{
    // Is there anything to draw at all?
    qreal angleLen = d->angleLens[dataset][slice];
    if (angleLen) {
        qreal startAngle = d->startAngles[dataset][slice];

        QModelIndex index(model()->index(dataset, slice, rootIndex())); // checked
        const PieAttributes attrs(pieAttributes(index));
        const ThreeDPieAttributes threeDAttrs(threeDPieAttributes(index));

        const int rCount = rowCount();
        const int colCount = columnCount();

        int iPoint = 0;

        QRectF drawPosition = d->position;

        painter->setRenderHint(QPainter::Antialiasing);

        QBrush br = brush(index);
        if (threeDAttrs.isEnabled()) {
            br = threeDAttrs.threeDBrush(br, drawPosition);
        }
        painter->setBrush(br);

        painter->setPen(pen(index));

        if (angleLen == 360) {
            // full circle, avoid nasty line in the middle
            // FIXME: Draw a complete ring here
            // painter->drawEllipse( drawPosition );
        } else {
            bool perfectMatch = false;

            qreal circularGap = 0.0;

            if (attrs.gapFactor(true) > 0.0) {
                // FIXME: Measure in degrees!
                circularGap = attrs.gapFactor(true);
            }

            QPolygonF poly;

            qreal degree = 0;

            qreal actualStartAngle = startAngle + circularGap;
            qreal actualAngleLen = angleLen - 2 * circularGap;

            qreal totalRadialExplode = 0.0;
            qreal maxRadialExplode = 0.0;

            qreal totalRadialGap = 0.0;
            qreal maxRadialGap = 0.0;
            for (uint i = rCount - 1; i > dataset; --i) {
                qreal maxRadialExplodeInThisRow = 0.0;
                qreal maxRadialGapInThisRow = 0.0;
                for (int j = 0; j < colCount; ++j) {
                    const PieAttributes cellAttrs(pieAttributes(model()->index(i, j, rootIndex()))); // checked
                    if (d->expandWhenExploded) {
                        maxRadialGapInThisRow = qMax(maxRadialGapInThisRow, cellAttrs.gapFactor(false));
                    }

                    // Don't use a gap for the very inner circle
                    if (cellAttrs.explode() && d->expandWhenExploded) {
                        maxRadialExplodeInThisRow = qMax(maxRadialExplodeInThisRow, cellAttrs.explodeFactor());
                    }
                }
                maxRadialExplode += maxRadialExplodeInThisRow;
                maxRadialGap += maxRadialGapInThisRow;

                // FIXME: What if explode factor of inner ring is > 1.0 ?
                // if ( !d->expandWhenExploded )
                //    break;
            }
            totalRadialGap = maxRadialGap + attrs.gapFactor(false);
            totalRadialExplode = attrs.explode() ? maxRadialExplode + attrs.explodeFactor() : maxRadialExplode;

            while (degree <= actualAngleLen) {
                const QPointF p = pointOnEllipse(drawPosition, dataset, slice, false, actualStartAngle + degree, totalRadialGap, totalRadialExplode);
                poly.append(p);
                degree += granularity;
                iPoint++;
            }
            if (!perfectMatch) {
                poly.append(pointOnEllipse(drawPosition, dataset, slice, false, actualStartAngle + actualAngleLen, totalRadialGap, totalRadialExplode));
                iPoint++;
            }

            // The center point of the inner brink
            const QPointF innerCenterPoint(poly[int(iPoint / 2)]);

            actualStartAngle = startAngle + circularGap;
            actualAngleLen = angleLen - 2 * circularGap;

            degree = actualAngleLen;

            const int lastInnerBrinkPoint = iPoint;
            while (degree >= 0) {
                poly.append(pointOnEllipse(drawPosition, dataset, slice, true, actualStartAngle + degree, totalRadialGap, totalRadialExplode));
                perfectMatch = (degree == 0);
                degree -= granularity;
                iPoint++;
            }
            // if necessary add one more point to fill the last small gap
            if (!perfectMatch) {
                poly.append(pointOnEllipse(drawPosition, dataset, slice, true, actualStartAngle, totalRadialGap, totalRadialExplode));
                iPoint++;
            }

            // The center point of the outer brink
            const QPointF outerCenterPoint(poly[lastInnerBrinkPoint + int((iPoint - lastInnerBrinkPoint) / 2)]);
            // qDebug() << poly;
            // find the value and paint it
            // fix value position
            const qreal sum = valueTotals(dataset);
            painter->drawPolygon(poly);

            d->reverseMapper.addPolygon(index.row(), index.column(), poly);

            const QPointF centerPoint = (innerCenterPoint + outerCenterPoint) / 2.0;

            const PainterSaver ps(painter);
            const TextAttributes ta = dataValueAttributes(index).textAttributes();
            if (!ta.hasRotation() && autoRotateLabels()) {
                const QPointF &p1 = poly.last();
                const QPointF &p2 = poly[lastInnerBrinkPoint];
                const QLineF line(p1, p2);
                // TODO: do the label rotation like in PieDiagram
                const qreal angle = line.dx() == 0 ? 0.0 : atan(line.dy() / line.dx());
                painter->translate(centerPoint);
                painter->rotate(angle / 2.0 / 3.141592653589793 * 360.0);
                painter->translate(-centerPoint);
            }

            paintDataValueText(painter, index, centerPoint, angleLen * sum / 360);
        }
    }
}

QPointF RingDiagram::pointOnEllipse(const QRectF &rect, int dataset, int slice, bool outer, qreal angle, qreal totalGapFactor, qreal totalExplodeFactor)
{
    qreal angleLen = d->angleLens[dataset][slice];
    qreal startAngle = d->startAngles[dataset][slice];

    const int rCount = rowCount() * 2;

    qreal level = outer ? (rCount - dataset - 1) + 2 : (rCount - dataset - 1) + 1;

    const qreal offsetX = rCount > 0 ? level * rect.width() / ((rCount + 1) * 2) : 0.0;
    const qreal offsetY = rCount > 0 ? level * rect.height() / ((rCount + 1) * 2) : 0.0;
    const qreal centerOffsetX = rCount > 0 ? totalExplodeFactor * rect.width() / ((rCount + 1) * 2) : 0.0;
    const qreal centerOffsetY = rCount > 0 ? totalExplodeFactor * rect.height() / ((rCount + 1) * 2) : 0.0;
    const qreal gapOffsetX = rCount > 0 ? totalGapFactor * rect.width() / ((rCount + 1) * 2) : 0.0;
    const qreal gapOffsetY = rCount > 0 ? totalGapFactor * rect.height() / ((rCount + 1) * 2) : 0.0;

    qreal explodeAngleRad = DEGTORAD(angle);
    qreal cosAngle = cos(explodeAngleRad);
    qreal sinAngle = -sin(explodeAngleRad);
    qreal explodeAngleCenterRad = DEGTORAD(startAngle + angleLen / 2.0);
    qreal cosAngleCenter = cos(explodeAngleCenterRad);
    qreal sinAngleCenter = -sin(explodeAngleCenterRad);
    return QPointF((offsetX + gapOffsetX) * cosAngle + centerOffsetX * cosAngleCenter + rect.center().x(),
                   (offsetY + gapOffsetY) * sinAngle + centerOffsetY * sinAngleCenter + rect.center().y());
}

/*virtual*/
qreal RingDiagram::valueTotals() const
{
    const int rCount = rowCount();
    const int colCount = columnCount();
    qreal total = 0.0;
    for (int i = 0; i < rCount; ++i) {
        for (int j = 0; j < colCount; ++j) {
            total += qAbs(model()->data(model()->index(i, j, rootIndex())).toReal()); // checked
        }
    }
    return total;
}

qreal RingDiagram::valueTotals(int dataset) const
{
    Q_ASSERT(dataset < model()->rowCount());
    const int colCount = columnCount();
    qreal total = 0.0;
    for (int j = 0; j < colCount; ++j) {
        total += qAbs(model()->data(model()->index(dataset, j, rootIndex())).toReal()); // checked
    }
    return total;
}

/*virtual*/
qreal RingDiagram::numberOfValuesPerDataset() const
{
    return model() ? model()->columnCount(rootIndex()) : 0.0;
}

qreal RingDiagram::numberOfDatasets() const
{
    return model() ? model()->rowCount(rootIndex()) : 0.0;
}

/*virtual*/
qreal RingDiagram::numberOfGridRings() const
{
    return 1;
}
