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

#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses

#include <QtCharts/QBarCategoryAxis>
#include <private/qbarcategoryaxis_p.h>
#include <private/chartbarcategoryaxisx_p.h>
#include <private/chartbarcategoryaxisy_p.h>
#include <private/abstractdomain_p.h>
#include <QtCharts/QChart>
#include <QtCore/QtMath>

QT_BEGIN_NAMESPACE
/*!
    \class QBarCategoryAxis
    \inmodule QtCharts
    \brief The QBarCategoryAxis class adds categories to a chart's axes.

    QBarCategoryAxis can be set up to show an axis line with tick marks, grid lines, and shades.
    Categories are drawn between the ticks. It can be used also with a line series, as demonstrated
    by the \l {Charts with Widgets Gallery}.

    The following code illustrates how to use QBarCategoryAxis:
    \code
    QChartView *chartView = new QChartView;
    QBarSeries *series = new QBarSeries;
    // ...
    chartView->chart()->addSeries(series);
    chartView->chart()->createDefaultAxes();

    QBarCategoryAxis *axisX = new QBarCategoryAxis;
    QStringList categories;
    categories << "Jan" << "Feb" << "Mar" << "Apr" << "May" << "Jun";
    axisX->append(categories);
    axisX->setRange("Feb", "May");
    chartView->chart()->setAxisX(axisX, series);
    \endcode
*/

/*!
    \qmltype BarCategoryAxis
    \nativetype QBarCategoryAxis
    \inqmlmodule QtCharts

    \inherits AbstractAxis

    \brief Adds categories to a chart's axes.

    The BarCategoryAxis type can be set up to show an axis line with tick marks, grid lines, and
    shades. Categories are drawn between the ticks. It can be used also with a line series.

    The following QML snippet illustrates how to use BarCategoryAxis:
    \code
        ChartView {
            BarCategoryAxis {
                id: categoryAxis
                categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun" ]
            }
        // Add a few series...
        }
    \endcode
*/

/*!
  \property QBarCategoryAxis::categories
  \brief The categories of an axis.
*/
/*!
  \qmlproperty list<string> BarCategoryAxis::categories
  The categories of an axis.
*/

/*!
  \property QBarCategoryAxis::min
  \brief The minimum value on the axis.
*/
/*!
  \qmlproperty string BarCategoryAxis::min
  The minimum value on the axis.
*/

/*!
  \property QBarCategoryAxis::max
  \brief The maximum value on the axis.
*/
/*!
  \qmlproperty string BarCategoryAxis::max
  The maximum value on the axis.
*/

/*!
  \property QBarCategoryAxis::count
  \brief The number of categories of an axis.
*/
/*!
  \qmlproperty int BarCategoryAxis::count
  The number of categories of an axis.
*/

/*!
  \fn void QBarCategoryAxis::categoriesChanged()
  This signal is emitted when the categories of the axis change.
*/

/*!
  \fn void QBarCategoryAxis::minChanged(const QString &min)
  This signal is emitted when the \a min value of the axis changes.
*/

/*!
  \fn void QBarCategoryAxis::maxChanged(const QString &max)
  This signal is emitted when the \a max value of the axis changes.
*/

/*!
  \fn void QBarCategoryAxis::countChanged()
  This signal is emitted when the number of categories of an axis changes.
*/

/*!
  \fn void QBarCategoryAxis::rangeChanged(const QString &min, const QString &max)
  This signal is emitted when \a min or \a max value of the axis changes.
*/

/*!
  \qmlsignal BarCategoryAxis::rangeChanged(string min, string max)
  This signal is emitted when \a min or \a max value of the axis changes.

  The corresponding signal handler is \c onRangeChanged.
*/

/*!
  \qmlmethod void BarCategoryAxis::clear()
  Removes all categories. Sets the maximum and minimum values of the axis range to QString::null.
*/

/*!
    Constructs an axis object that is the child of \a parent.
*/
QBarCategoryAxis::QBarCategoryAxis(QObject *parent):
    QAbstractAxis(*new QBarCategoryAxisPrivate(this), parent)
{
}

/*!
    Destroys the axis object.
*/
QBarCategoryAxis::~QBarCategoryAxis()
{
    Q_D(QBarCategoryAxis);
    if (d->m_chart)
        d->m_chart->removeAxis(this);
}

/*!
    \internal
*/
QBarCategoryAxis::QBarCategoryAxis(QBarCategoryAxisPrivate &d, QObject *parent)
    : QAbstractAxis(d, parent)
{

}

/*!
    Appends \a categories to an axis. The maximum value on the axis will be changed
    to match the last category in \a categories. If no categories were previously defined,
    the minimum value on the axis will also be changed to match the first category in
    \a categories.

    A category has to be a valid QString and it cannot be duplicated. Duplicated
    categories will not be appended.
*/
void QBarCategoryAxis::append(const QStringList &categories)
{
    if (categories.isEmpty())
        return;

    Q_D(QBarCategoryAxis);

    int count = d->m_categories.size();

    foreach(QString category, categories) {
        if (!d->m_categories.contains(category) && !category.isNull()) {
            d->m_categories.append(category);
        }
    }

    if (d->m_categories.size() == count)
        return;

    if (count == 0)
        setRange(d->m_categories.first(), d->m_categories.last());
    else
        setRange(d->m_minCategory, d->m_categories.last());

    emit categoriesChanged();
    emit countChanged();
}

/*!
    Appends \a category to an axis. The maximum value on the axis will be changed
    to match the last \a  category. If no categories were previously defined, the minimum
    value on the axis will also be changed to match \a category.

    A category has to be a valid QString and it cannot be duplicated. Duplicated
    categories will not be appended.
*/
void QBarCategoryAxis::append(const QString &category)
{
    Q_D(QBarCategoryAxis);

    int count = d->m_categories.size();

    if (!d->m_categories.contains(category) && !category.isNull())
        d->m_categories.append(category);

    if (d->m_categories.size() == count)
        return;

    if (count == 0)
        setRange(d->m_categories.last(), d->m_categories.last());
    else
        setRange(d->m_minCategory, d->m_categories.last());

    emit categoriesChanged();
    emit countChanged();
}

/*!
    Removes \a category from the axis. Removing a category that currently sets the
    maximum or minimum value on the axis will affect the axis range.
*/
void QBarCategoryAxis::remove(const QString &category)
{
    Q_D(QBarCategoryAxis);

    if (d->m_categories.contains(category)) {
        d->m_categories.removeAt(d->m_categories.indexOf(category));
        if (!d->m_categories.isEmpty()) {
            if (d->m_minCategory == category) {
                setRange(d->m_categories.first(), d->m_maxCategory);
            } else if (d->m_maxCategory == category) {
                setRange(d->m_minCategory, d->m_categories.last());
            } else {
                d->updateCategoryDomain();
            }
        } else {
            setRange(QString(), QString());
        }
        emit categoriesChanged();
        emit countChanged();
    }
}

/*!
    Inserts \a category to the axis at \a index. \a category has to be a valid QString
    and it cannot be duplicated. If \a category is prepended or appended to other
    categories, the minimum and maximum values on the axis are updated accordingly.
*/
void QBarCategoryAxis::insert(int index, const QString &category)
{
    Q_D(QBarCategoryAxis);

    int count = d->m_categories.size();

    if (!d->m_categories.contains(category) && !category.isNull())
        d->m_categories.insert(index, category);

    if (d->m_categories.size() == count)
        return;

    if (count == 0) {
        setRange(d->m_categories.first(), d->m_categories.first());
    } else if (index == 0) {
        setRange(d->m_categories.first(), d->m_maxCategory);
    } else if (index == count) {
        setRange(d->m_minCategory, d->m_categories.last());
    } else {
        d->updateCategoryDomain();
    }

    emit categoriesChanged();
    emit countChanged();
}

/*!
    Replaces \a oldCategory with \a newCategory. If \a oldCategory does not exist on the axis,
    nothing is done. \a newCategory has to be a valid QString and it cannot be duplicated. If
    the minimum or maximum category is replaced, the minimum and maximum values on the axis are
    updated accordingly.
*/
void QBarCategoryAxis::replace(const QString &oldCategory, const QString &newCategory)
{
    Q_D(QBarCategoryAxis);

    int pos = d->m_categories.indexOf(oldCategory);

    if (pos != -1 && !d->m_categories.contains(newCategory) && !newCategory.isNull()) {
        d->m_categories.replace(pos, newCategory);
        if (d->m_minCategory == oldCategory)
            setRange(newCategory, d->m_maxCategory);
        else if (d->m_maxCategory == oldCategory)
            setRange(d->m_minCategory, newCategory);

        emit categoriesChanged();
        emit countChanged();
    }
}

/*!
    Removes all categories. Sets the maximum and minimum values of the axis range to QString::null.
 */
void QBarCategoryAxis::clear()
{
    Q_D(QBarCategoryAxis);
    d->m_categories.clear();
    setRange(QString(), QString());
    emit categoriesChanged();
    emit countChanged();
}

/*!
    Sets \a categories and discards the old ones. The axis range is adjusted to match the
    first and last category in \a categories.

    A category has to be a valid QString and it cannot be duplicated.
*/
void QBarCategoryAxis::setCategories(const QStringList &categories)
{
    Q_D(QBarCategoryAxis);
    d->m_categories.clear();
    d->m_minCategory = QString();
    d->m_maxCategory = QString();
    d->m_min = 0;
    d->m_max = 0;
    d->m_count = 0;
    append(categories);
}

/*!
    Returns categories.
*/
QStringList QBarCategoryAxis::categories()
{
    Q_D(QBarCategoryAxis);
    return d->m_categories;
}

/*!
    Returns the number of categories.
 */
int QBarCategoryAxis::count() const
{
    Q_D(const QBarCategoryAxis);
    return d->m_categories.size();
}

/*!
    Returns the category at \a index. The index must be valid.
*/
QString QBarCategoryAxis::at(int index) const
{
    Q_D(const QBarCategoryAxis);
    return d->m_categories.at(index);
}

/*!
    Sets the minimum category to \a min.
*/
void QBarCategoryAxis::setMin(const QString &min)
{
    Q_D(QBarCategoryAxis);
    d->setRange(min, d->m_maxCategory);
}

/*!
    Returns the minimum category.
*/
QString QBarCategoryAxis::min() const
{
    Q_D(const QBarCategoryAxis);
    return d->m_minCategory;
}

/*!
    Sets the maximum category to \a max.
*/
void QBarCategoryAxis::setMax(const QString &max)
{
    Q_D(QBarCategoryAxis);
    d->setRange(d->m_minCategory, max);
}

/*!
    Returns the maximum category.
*/
QString QBarCategoryAxis::max() const
{
    Q_D(const QBarCategoryAxis);
    return d->m_maxCategory;
}

/*!
    Sets the axis range from \a minCategory to \a maxCategory.
*/
void QBarCategoryAxis::setRange(const QString &minCategory, const QString &maxCategory)
{
    Q_D(QBarCategoryAxis);
    d->setRange(minCategory,maxCategory);
}

/*!
    Returns the type of the axis.
*/
QAbstractAxis::AxisType QBarCategoryAxis::type() const
{
    return AxisTypeBarCategory;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

QBarCategoryAxisPrivate::QBarCategoryAxisPrivate(QBarCategoryAxis *q)
    : QAbstractAxisPrivate(q),
      m_min(0.0),
      m_max(0.0),
      m_count(0)
{

}

QBarCategoryAxisPrivate::~QBarCategoryAxisPrivate()
{

}

void QBarCategoryAxisPrivate::setMin(const QVariant &min)
{
    setRange(min, m_maxCategory);
}

void QBarCategoryAxisPrivate::setMax(const QVariant &max)
{
    setRange(m_minCategory, max);
}

void QBarCategoryAxisPrivate::setRange(const QVariant &min, const QVariant &max)
{
    QString value1 = min.toString();
    QString value2 = max.toString();
    setRange(value1, value2);
}

void QBarCategoryAxisPrivate::setRange(qreal min, qreal max)
{
    Q_Q(QBarCategoryAxis);

    bool categoryChanged = false;
    bool changed = false;

    if (min > max)
        return;

    if (!qFuzzyIsNull(m_min - min)) {
        m_min = min;
        changed = true;

        int imin = m_min + 0.5;
        if (imin >= 0 && imin < m_categories.size()) {
            QString minCategory = m_categories.at(imin);
            if (m_minCategory != minCategory && !minCategory.isEmpty()) {
                m_minCategory = minCategory;
                categoryChanged = true;
                emit q->minChanged(minCategory);
            }
        }

    }

    if (!qFuzzyIsNull(m_max - max)) {
        m_max = max;
        changed = true;

        int imax = m_max - 0.5;
        if (imax >= 0 && imax < m_categories.size()) {
            QString maxCategory = m_categories.at(imax);
            if (m_maxCategory != maxCategory && !maxCategory.isEmpty()) {
                m_maxCategory = maxCategory;
                categoryChanged = true;
                emit q->maxChanged(maxCategory);
            }
        }
    }

    if (categoryChanged){
        emit q->rangeChanged(m_minCategory, m_maxCategory);
    }

    if (changed) {
        emit rangeChanged(m_min,m_max);
    }
}

void  QBarCategoryAxisPrivate::setRange(const QString &minCategory, const QString &maxCategory)
{
    Q_Q(QBarCategoryAxis);
    bool changed = false;

    //special case in case or clearing all categories
    if (minCategory.isNull() && maxCategory.isNull()) {
        m_minCategory = minCategory;
        m_maxCategory = maxCategory;
        m_min = 0;
        m_max = 0;
        m_count = 0;
        emit q->minChanged(minCategory);
        emit q->maxChanged(maxCategory);
        emit q->rangeChanged(m_minCategory, m_maxCategory);
        emit rangeChanged(m_min,m_max);
        return;
    }

    if (m_categories.indexOf(maxCategory) < m_categories.indexOf(minCategory))
        return;

    if (!minCategory.isNull() && (m_minCategory != minCategory || m_minCategory.isNull())
            && m_categories.contains(minCategory)) {
        m_minCategory = minCategory;
        m_min = m_categories.indexOf(m_minCategory) - 0.5;
        changed = true;
        emit q->minChanged(minCategory);
    }

    if (!maxCategory.isNull() && (m_maxCategory != maxCategory || m_maxCategory.isNull())
            && m_categories.contains(maxCategory)) {
        m_maxCategory = maxCategory;
        m_max = m_categories.indexOf(m_maxCategory) + 0.5;
        changed = true;
        emit q->maxChanged(maxCategory);
    }

    if (changed) {
        m_count = m_max - m_min;
        emit q->rangeChanged(m_minCategory, m_maxCategory);
        emit rangeChanged(m_min,m_max);
    }
}

void QBarCategoryAxisPrivate::initializeGraphics(QGraphicsItem* parent)
{
    Q_Q(QBarCategoryAxis);
    ChartAxisElement* axis(0);
    if (orientation() == Qt::Vertical)
        axis = new ChartBarCategoryAxisY(q,parent);
    if (orientation() == Qt::Horizontal)
        axis = new ChartBarCategoryAxisX(q,parent);

    m_item.reset(axis);
    QAbstractAxisPrivate::initializeGraphics(parent);
}

void QBarCategoryAxisPrivate::updateCategoryDomain()
{
    bool changed = false;

    qreal tmpMin = m_categories.indexOf(m_minCategory) - 0.5;
    if (!qFuzzyIsNull(m_min - tmpMin)) {
        m_min = tmpMin;
        changed = true;
    }
    qreal tmpMax = m_categories.indexOf(m_maxCategory) + 0.5;
    if (!qFuzzyIsNull(m_max - tmpMax)) {
        m_max = tmpMax;
        changed = true;
    }
    m_count = m_max - m_min;

    if (changed)
        emit rangeChanged(m_min,m_max);
}


void QBarCategoryAxisPrivate::initializeDomain(AbstractDomain *domain)
{
    Q_Q(QBarCategoryAxis);
    if (m_max == m_min) {
        int min;
        int max;
        if (orientation() == Qt::Vertical) {
            min = domain->minY() + 0.5;
            max = domain->maxY() - 0.5;
        } else {
            min = domain->minX() + 0.5;
            max = domain->maxX() - 0.5;
        }

        if (min > 0 && min < m_categories.size() && max > 0 && max < m_categories.size())
            q->setRange(m_categories.at(min), m_categories.at(max));
    } else {
        if (orientation() == Qt::Vertical)
            domain->setRangeY(m_min, m_max);
        else
            domain->setRangeX(m_min, m_max);
    }
}

QT_END_NAMESPACE

#include "moc_qbarcategoryaxis.cpp"
#include "moc_qbarcategoryaxis_p.cpp"
