/******************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the Qt Data Visualization module.
**
** $QT_BEGIN_LICENSE:COMM$
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** $QT_END_LICENSE$
**
******************************************************************************/

#include "qheightmapsurfacedataproxy_p.h"

QT_BEGIN_NAMESPACE_DATAVISUALIZATION

// Default ranges correspond value axis defaults
const float defaultMinValue = 0.0f;
const float defaultMaxValue = 10.0f;

/*!
 * \class QHeightMapSurfaceDataProxy
 * \inmodule QtDataVisualization
 * \brief Base proxy class for Q3DSurface.
 * \since QtDataVisualization 1.0
 *
 * QHeightMapSurfaceDataProxy takes care of surface related height map data handling. It provides a
 * way to give a height map to be visualized as a surface plot.
 *
 * Since height maps do not contain values for X or Z axes, those values need to be given
 * separately using minXValue, maxXValue, minZValue, and maxZValue properties. X-value corresponds
 * to image horizontal direction and Z-value to the vertical. Setting any of these
 * properties triggers asynchronous re-resolving of any existing height map.
 *
 * \sa QSurfaceDataProxy, {Qt Data Visualization Data Handling}
 */

/*!
 * \qmltype HeightMapSurfaceDataProxy
 * \inqmlmodule QtDataVisualization
 * \since QtDataVisualization 1.0
 * \ingroup datavisualization_qml
 * \instantiates QHeightMapSurfaceDataProxy
 * \inherits SurfaceDataProxy
 * \brief Base proxy type for Surface3D.
 *
 * HeightMapSurfaceDataProxy takes care of surface related height map data handling. It provides a
 * way to give a height map to be visualized as a surface plot.
 *
 * For more complete description, see QHeightMapSurfaceDataProxy.
 *
 * \sa {Qt Data Visualization Data Handling}
 */

/*!
 * \qmlproperty string HeightMapSurfaceDataProxy::heightMapFile
 *
 * A file with a height map image to be visualized. Setting this property replaces current data
 * with height map data.
 *
 * There are several formats the image file can be given in, but if it is not in a directly usable
 * format, a conversion is made. \note If the result seems wrong, the automatic conversion failed
 * and you should try converting the image yourself before setting it. Preferred format is
 * QImage::Format_RGB32 in grayscale.
 *
 * The height of the image is read from the red component of the pixels if the image is in grayscale,
 * otherwise it is an average calculated from red, green and blue components of the pixels. Using
 * grayscale images may improve data conversion speed for large images.
 *
 * Since height maps do not contain values for X or Z axes, those values need to be given
 * separately using minXValue, maxXValue, minZValue, and maxZValue properties. X-value corresponds
 * to image horizontal direction and Z-value to the vertical. Setting any of these
 * properties triggers asynchronous re-resolving of any existing height map.
 *
 * Not recommended formats: all mono formats (for example QImage::Format_Mono).
 */

/*!
 * \qmlproperty real HeightMapSurfaceDataProxy::minXValue
 *
 * The minimum X value for the generated surface points. Defaults to \c{0.0}.
 * When setting this property the corresponding maximum value is adjusted if necessary,
 * to ensure that the range remains valid.
 */

/*!
 * \qmlproperty real HeightMapSurfaceDataProxy::maxXValue
 *
 * The maximum X value for the generated surface points. Defaults to \c{10.0}.
 * When setting this property the corresponding minimum value is adjusted if necessary,
 * to ensure that the range remains valid.
 */

/*!
 * \qmlproperty real HeightMapSurfaceDataProxy::minZValue
 *
 * The minimum Z value for the generated surface points. Defaults to \c{0.0}.
 * When setting this property the corresponding maximum value is adjusted if necessary,
 * to ensure that the range remains valid.
 */

/*!
 * \qmlproperty real HeightMapSurfaceDataProxy::maxZValue
 *
 * The maximum Z value for the generated surface points. Defaults to \c{10.0}.
 * When setting this property the corresponding minimum value is adjusted if necessary,
 * to ensure that the range remains valid.
 */

/*!
 * Constructs QHeightMapSurfaceDataProxy with the given \a parent.
 */
QHeightMapSurfaceDataProxy::QHeightMapSurfaceDataProxy(QObject *parent) :
    QSurfaceDataProxy(new QHeightMapSurfaceDataProxyPrivate(this), parent)
{
}

/*!
 * Constructs QHeightMapSurfaceDataProxy with the given \a image and \a parent. Height map is set
 * by calling setHeightMap() with \a image.
 *
 * \sa heightMap
 */
QHeightMapSurfaceDataProxy::QHeightMapSurfaceDataProxy(const QImage &image, QObject *parent) :
    QSurfaceDataProxy(new QHeightMapSurfaceDataProxyPrivate(this), parent)
{
    setHeightMap(image);
}

/*!
 * Constructs QHeightMapSurfaceDataProxy from the given image \a filename and \a parent. Height map is set
 * by calling setHeightMapFile() with \a filename.
 *
 * \sa heightMapFile
 */
QHeightMapSurfaceDataProxy::QHeightMapSurfaceDataProxy(const QString &filename, QObject *parent) :
    QSurfaceDataProxy(new QHeightMapSurfaceDataProxyPrivate(this), parent)
{
    setHeightMapFile(filename);
}

/*!
 * \internal
 */
QHeightMapSurfaceDataProxy::QHeightMapSurfaceDataProxy(
        QHeightMapSurfaceDataProxyPrivate *d, QObject *parent) :
    QSurfaceDataProxy(d, parent)
{
}

/*!
 * Destroys QHeightMapSurfaceDataProxy.
 */
QHeightMapSurfaceDataProxy::~QHeightMapSurfaceDataProxy()
{
}

/*!
 * \property QHeightMapSurfaceDataProxy::heightMap
 *
 * A height map \a image to be visualized. Setting this property replaces current data with height
 * map data.
 *
 * There are several formats the \a image can be given in, but if it is not in a directly usable
 * format, a conversion is made. \note If the result seems wrong, the automatic conversion failed
 * and you should try converting the \a image yourself before setting it. Preferred format is
 * QImage::Format_RGB32 in grayscale.
 *
 * The height of the \a image is read from the red component of the pixels if the \a image is in
 * grayscale, otherwise it is an average calculated from red, green, and blue components of the
 * pixels. Using grayscale images may improve data conversion speed for large images.
 *
 * Not recommended formats: all mono formats (for example QImage::Format_Mono).
 *
 * The height map is resolved asynchronously. QSurfaceDataProxy::arrayReset() is emitted when the
 * data has been resolved.
 */
void QHeightMapSurfaceDataProxy::setHeightMap(const QImage &image)
{
    dptr()->m_heightMap = image;

    // We do resolving asynchronously to make qml onArrayReset handlers actually get the initial reset
    if (!dptr()->m_resolveTimer.isActive())
        dptr()->m_resolveTimer.start(0);
}

QImage QHeightMapSurfaceDataProxy::heightMap() const
{
    return dptrc()->m_heightMap;
}

/*!
 * \property QHeightMapSurfaceDataProxy::heightMapFile
 *
 * A file with a height map image to be visualized. Setting this property replaces current data
 * with height map data.
 *
 * \sa heightMap
 */
void QHeightMapSurfaceDataProxy::setHeightMapFile(const QString &filename)
{
    dptr()->m_heightMapFile = filename;
    setHeightMap(QImage(filename));
    emit heightMapFileChanged(filename);
}

QString QHeightMapSurfaceDataProxy::heightMapFile() const
{
    return dptrc()->m_heightMapFile;
}

/*!
 * A convenience function for setting all minimum (\a minX and \a minZ) and maximum
 * (\a maxX and \a maxZ) values at the same time. The minimum values must be smaller than the
 * corresponding maximum value. Otherwise the values get adjusted so that they are valid.
 */
void QHeightMapSurfaceDataProxy::setValueRanges(float minX, float maxX, float minZ, float maxZ)
{
    dptr()->setValueRanges(minX, maxX, minZ, maxZ);
}

/*!
 * \property QHeightMapSurfaceDataProxy::minXValue
 *
 * The minimum X value for the generated surface points. Defaults to \c{0.0}.
 * When setting this property the corresponding maximum value is adjusted if necessary,
 * to ensure that the range remains valid.
 */
void QHeightMapSurfaceDataProxy::setMinXValue(float min)
{
    dptr()->setMinXValue(min);
}

float QHeightMapSurfaceDataProxy::minXValue() const
{
    return dptrc()->m_minXValue;
}

/*!
 * \property QHeightMapSurfaceDataProxy::maxXValue
 *
 * The maximum X value for the generated surface points. Defaults to \c{10.0}.
 * When setting this property the corresponding minimum value is adjusted if necessary,
 * to ensure that the range remains valid.
 */
void QHeightMapSurfaceDataProxy::setMaxXValue(float max)
{
    dptr()->setMaxXValue(max);
}

float QHeightMapSurfaceDataProxy::maxXValue() const
{
    return dptrc()->m_maxXValue;
}

/*!
 * \property QHeightMapSurfaceDataProxy::minZValue
 *
 * The minimum Z value for the generated surface points. Defaults to \c{0.0}.
 * When setting this property the corresponding maximum value is adjusted if necessary,
 * to ensure that the range remains valid.
 */
void QHeightMapSurfaceDataProxy::setMinZValue(float min)
{
    dptr()->setMinZValue(min);
}

float QHeightMapSurfaceDataProxy::minZValue() const
{
    return dptrc()->m_minZValue;
}

/*!
 * \property QHeightMapSurfaceDataProxy::maxZValue
 *
 * The maximum Z value for the generated surface points. Defaults to \c{10.0}.
 * When setting this property the corresponding minimum value is adjusted if necessary,
 * to ensure that the range remains valid.
 */
void QHeightMapSurfaceDataProxy::setMaxZValue(float max)
{
    dptr()->setMaxZValue(max);
}

float QHeightMapSurfaceDataProxy::maxZValue() const
{
    return dptrc()->m_maxZValue;
}

/*!
 * \internal
 */
QHeightMapSurfaceDataProxyPrivate *QHeightMapSurfaceDataProxy::dptr()
{
    return static_cast<QHeightMapSurfaceDataProxyPrivate *>(d_ptr.data());
}

/*!
 * \internal
 */
const QHeightMapSurfaceDataProxyPrivate *QHeightMapSurfaceDataProxy::dptrc() const
{
    return static_cast<const QHeightMapSurfaceDataProxyPrivate *>(d_ptr.data());
}

//  QHeightMapSurfaceDataProxyPrivate

QHeightMapSurfaceDataProxyPrivate::QHeightMapSurfaceDataProxyPrivate(QHeightMapSurfaceDataProxy *q)
    : QSurfaceDataProxyPrivate(q),
      m_minXValue(defaultMinValue),
      m_maxXValue(defaultMaxValue),
      m_minZValue(defaultMinValue),
      m_maxZValue(defaultMaxValue)
{
    m_resolveTimer.setSingleShot(true);
    QObject::connect(&m_resolveTimer, &QTimer::timeout,
                     this, &QHeightMapSurfaceDataProxyPrivate::handlePendingResolve);
}

QHeightMapSurfaceDataProxyPrivate::~QHeightMapSurfaceDataProxyPrivate()
{
}

QHeightMapSurfaceDataProxy *QHeightMapSurfaceDataProxyPrivate::qptr()
{
    return static_cast<QHeightMapSurfaceDataProxy *>(q_ptr);
}

void QHeightMapSurfaceDataProxyPrivate::setValueRanges(float minX, float maxX,
                                                       float minZ, float maxZ)
{
    bool minXChanged = false;
    bool maxXChanged = false;
    bool minZChanged = false;
    bool maxZChanged = false;
    if (m_minXValue != minX) {
        m_minXValue = minX;
        minXChanged = true;
    }
    if (m_minZValue != minZ) {
        m_minZValue = minZ;
        minZChanged = true;
    }
    if (m_maxXValue != maxX || minX >= maxX) {
        if (minX >= maxX) {
            m_maxXValue = minX + 1.0f;
            qWarning() << "Warning: Tried to set invalid range for X value range."
                          " Range automatically adjusted to a valid one:"
                       << minX << "-" << maxX << "-->" << m_minXValue << "-" << m_maxXValue;
        } else {
            m_maxXValue = maxX;
        }
        maxXChanged = true;
    }
    if (m_maxZValue != maxZ || minZ >= maxZ) {
        if (minZ >= maxZ) {
            m_maxZValue = minZ + 1.0f;
            qWarning() << "Warning: Tried to set invalid range for Z value range."
                          " Range automatically adjusted to a valid one:"
                       << minZ << "-" << maxZ << "-->" << m_minZValue << "-" << m_maxZValue;
        } else {
            m_maxZValue = maxZ;
        }
        maxZChanged = true;
    }

    if (minXChanged)
        emit qptr()->minXValueChanged(m_minXValue);
    if (minZChanged)
        emit qptr()->minZValueChanged(m_minZValue);
    if (maxXChanged)
        emit qptr()->maxXValueChanged(m_maxXValue);
    if (maxZChanged)
        emit qptr()->maxZValueChanged(m_maxZValue);

    if ((minXChanged || minZChanged || maxXChanged || maxZChanged) && !m_resolveTimer.isActive())
        m_resolveTimer.start(0);
}

void QHeightMapSurfaceDataProxyPrivate::setMinXValue(float min)
{
    if (min != m_minXValue) {
        bool maxChanged = false;
        if (min >= m_maxXValue) {
            float oldMax = m_maxXValue;
            m_maxXValue = min + 1.0f;
            qWarning() << "Warning: Tried to set minimum X to equal or larger than maximum X for"
                          " value range. Maximum automatically adjusted to a valid one:"
                       << oldMax <<  "-->" << m_maxXValue;
            maxChanged = true;
        }
        m_minXValue = min;
        emit qptr()->minXValueChanged(m_minXValue);
        if (maxChanged)
            emit qptr()->maxXValueChanged(m_maxXValue);

        if (!m_resolveTimer.isActive())
            m_resolveTimer.start(0);
    }
}

void QHeightMapSurfaceDataProxyPrivate::setMaxXValue(float max)
{
    if (m_maxXValue != max) {
        bool minChanged = false;
        if (max <= m_minXValue) {
            float oldMin = m_minXValue;
            m_minXValue = max - 1.0f;
            qWarning() << "Warning: Tried to set maximum X to equal or smaller than minimum X for"
                          " value range. Minimum automatically adjusted to a valid one:"
                       << oldMin <<  "-->" << m_minXValue;
            minChanged = true;
        }
        m_maxXValue = max;
        emit qptr()->maxXValueChanged(m_maxXValue);
        if (minChanged)
            emit qptr()->minXValueChanged(m_minXValue);

        if (!m_resolveTimer.isActive())
            m_resolveTimer.start(0);
    }
}

void QHeightMapSurfaceDataProxyPrivate::setMinZValue(float min)
{
    if (min != m_minZValue) {
        bool maxChanged = false;
        if (min >= m_maxZValue) {
            float oldMax = m_maxZValue;
            m_maxZValue = min + 1.0f;
            qWarning() << "Warning: Tried to set minimum Z to equal or larger than maximum Z for"
                          " value range. Maximum automatically adjusted to a valid one:"
                       << oldMax <<  "-->" << m_maxZValue;
            maxChanged = true;
        }
        m_minZValue = min;
        emit qptr()->minZValueChanged(m_minZValue);
        if (maxChanged)
            emit qptr()->maxZValueChanged(m_maxZValue);

        if (!m_resolveTimer.isActive())
            m_resolveTimer.start(0);
    }
}

void QHeightMapSurfaceDataProxyPrivate::setMaxZValue(float max)
{
    if (m_maxZValue != max) {
        bool minChanged = false;
        if (max <= m_minZValue) {
            float oldMin = m_minZValue;
            m_minZValue = max - 1.0f;
            qWarning() << "Warning: Tried to set maximum Z to equal or smaller than minimum Z for"
                          " value range. Minimum automatically adjusted to a valid one:"
                       << oldMin <<  "-->" << m_minZValue;
            minChanged = true;
        }
        m_maxZValue = max;
        emit qptr()->maxZValueChanged(m_maxZValue);
        if (minChanged)
            emit qptr()->minZValueChanged(m_minZValue);

        if (!m_resolveTimer.isActive())
            m_resolveTimer.start(0);
    }
}

void QHeightMapSurfaceDataProxyPrivate::handlePendingResolve()
{
    QImage heightImage = m_heightMap;

    // Convert to RGB32 to be sure we're reading the right bytes
    if (heightImage.format() != QImage::Format_RGB32)
        heightImage = heightImage.convertToFormat(QImage::Format_RGB32);

    uchar *bits = heightImage.bits();

    int imageHeight = heightImage.height();
    int imageWidth = heightImage.width();
    int bitCount = imageWidth * 4 * (imageHeight - 1);
    int widthBits = imageWidth * 4;
    float height = 0;

    // Do not recreate array if dimensions have not changed
    QSurfaceDataArray *dataArray = m_dataArray;
    if (imageWidth != qptr()->columnCount() || imageHeight != dataArray->size()) {
        dataArray = new QSurfaceDataArray;
        dataArray->reserve(imageHeight);
        for (int i = 0; i < imageHeight; i++) {
            QSurfaceDataRow *newProxyRow = new QSurfaceDataRow(imageWidth);
            dataArray->append(newProxyRow);
        }
    }

    float xMul = (m_maxXValue - m_minXValue) / float(imageWidth - 1);
    float zMul = (m_maxZValue - m_minZValue) / float(imageHeight - 1);

    // Last row and column are explicitly set to max values, as relying
    // on multiplier can cause rounding errors, resulting in the value being
    // slightly over the specified maximum, which in turn can lead to it not
    // getting rendered.
    int lastRow = imageHeight - 1;
    int lastCol = imageWidth - 1;

    if (heightImage.isGrayscale()) {
        // Grayscale, it's enough to read Red byte
        for (int i = 0; i < imageHeight; i++, bitCount -= widthBits) {
            QSurfaceDataRow &newRow = *dataArray->at(i);
            float zVal;
            if (i == lastRow)
                zVal = m_maxZValue;
            else
                zVal = (float(i) * zMul) + m_minZValue;
            int j = 0;
            for (; j < lastCol; j++)
                newRow[j].setPosition(QVector3D((float(j) * xMul) + m_minXValue,
                                                float(bits[bitCount + (j * 4)]),
                                      zVal));
            newRow[j].setPosition(QVector3D(m_maxXValue,
                                            float(bits[bitCount + (j * 4)]),
                                  zVal));
        }
    } else {
        // Not grayscale, we'll need to calculate height from RGB
        for (int i = 0; i < imageHeight; i++, bitCount -= widthBits) {
            QSurfaceDataRow &newRow = *dataArray->at(i);
            float zVal;
            if (i == lastRow)
                zVal = m_maxZValue;
            else
                zVal = (float(i) * zMul) + m_minZValue;
            int j = 0;
            int nextpixel = 0;
            for (; j < lastCol; j++) {
                nextpixel = j * 4;
                height = (float(bits[bitCount + nextpixel])
                        + float(bits[1 + bitCount + nextpixel])
                        + float(bits[2 + bitCount + nextpixel]));
                newRow[j].setPosition(QVector3D((float(j) * xMul) + m_minXValue,
                                                height / 3.0f,
                                                zVal));
            }
            nextpixel = j * 4;
            height = (float(bits[bitCount + nextpixel])
                    + float(bits[1 + bitCount + nextpixel])
                    + float(bits[2 + bitCount + nextpixel]));
            newRow[j].setPosition(QVector3D(m_maxXValue,
                                            height / 3.0f,
                                            zVal));
        }
    }

    qptr()->resetArray(dataArray);
    emit qptr()->heightMapChanged(m_heightMap);
}

QT_END_NAMESPACE_DATAVISUALIZATION
