/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the demonstration applications of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** 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 https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**   * Redistributions of source code must retain the above copyright
**     notice, this list of conditions and the following disclaimer.
**   * Redistributions in binary form must reproduce the above copyright
**     notice, this list of conditions and the following disclaimer in
**     the documentation and/or other materials provided with the
**     distribution.
**   * Neither the name of The Qt Company Ltd nor the names of its
**     contributors may be used to endorse or promote products derived
**     from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QtCore>
#include <QtWidgets>
#include <qmath.h>

#define WORLD_SIZE 8
int world_map[WORLD_SIZE][WORLD_SIZE] = {
    { 1, 1, 1, 1, 6, 1, 1, 1 },
    { 1, 0, 0, 1, 0, 0, 0, 7 },
    { 1, 1, 0, 1, 0, 1, 1, 1 },
    { 6, 0, 0, 0, 0, 0, 0, 3 },
    { 1, 8, 8, 0, 8, 0, 8, 1 },
    { 2, 2, 0, 0, 8, 8, 7, 1 },
    { 3, 0, 0, 0, 0, 0, 0, 5 },
    { 2, 2, 2, 2, 7, 4, 4, 4 },
};

#define TEXTURE_SIZE 64
#define TEXTURE_BLOCK (TEXTURE_SIZE * TEXTURE_SIZE)

class Raycasting: public QWidget
{
public:
    Raycasting(QWidget *parent = nullptr)
            : QWidget(parent)
            , angle(0.5)
            , playerPos(1.5, 1.5)
            , angleDelta(0)
            , moveDelta(0)
            , touchDevice(false) {

        // http://www.areyep.com/RIPandMCS-TextureLibrary.html
        textureImg.load(":/textures.png");
        textureImg = textureImg.convertToFormat(QImage::Format_ARGB32);
        Q_ASSERT(textureImg.width() == TEXTURE_SIZE * 2);
        Q_ASSERT(textureImg.bytesPerLine() == 4 * TEXTURE_SIZE * 2);
        textureCount = textureImg.height() / TEXTURE_SIZE;

        watch.start();
        ticker.start(25, this);
        setAttribute(Qt::WA_OpaquePaintEvent, true);
        setMouseTracking(false);
    }

    void updatePlayer() {
        int interval = qBound(20ll, watch.elapsed(), 250ll);
        watch.start();
        angle += angleDelta * interval / 1000;
        qreal step = moveDelta * interval / 1000;
        qreal dx = cos(angle) * step;
        qreal dy = sin(angle) * step;
        QPointF pos = playerPos + 3 * QPointF(dx, dy);
        int xi = static_cast<int>(pos.x());
        int yi = static_cast<int>(pos.y());
        if (world_map[yi][xi] == 0)
            playerPos = playerPos + QPointF(dx, dy);
    }

    void showFps() {
        static QElapsedTimer frameTick;
        static int totalFrame = 0;
        if (!(totalFrame & 31)) {
            const qint64 elapsed = frameTick.elapsed();
            frameTick.start();
            int fps = 32 * 1000 / (1 + elapsed);
            setWindowTitle(QString("Raycasting (%1 FPS)").arg(fps));
        }
        totalFrame++;
    }

    void render() {

        // setup the screen surface
        if (buffer.size() != bufferSize)
            buffer = QImage(bufferSize, QImage::Format_ARGB32);
        int bufw = buffer.width();
        int bufh = buffer.height();
        if (bufw <= 0 || bufh <= 0)
            return;

        // we intentionally cheat here, to avoid detach
        const uchar *ptr = buffer.bits();
        QRgb *start = (QRgb*)(ptr);
        QRgb stride = buffer.bytesPerLine() / 4;
        QRgb *finish = start + stride * bufh;

        // prepare the texture pointer
        const uchar *src = textureImg.bits();
        const QRgb *texsrc = reinterpret_cast<const QRgb*>(src);

        // cast all rays here
        qreal sina = sin(angle);
        qreal cosa = cos(angle);
        qreal u = cosa - sina;
        qreal v = sina + cosa;
        qreal du = 2 * sina / bufw;
        qreal dv = -2 * cosa / bufw;

        for (int ray = 0; ray < bufw; ++ray, u += du, v += dv) {
            // every time this ray advances 'u' units in x direction,
            // it also advanced 'v' units in y direction
            qreal uu = (u < 0) ? -u : u;
            qreal vv = (v < 0) ? -v : v;
            qreal duu = 1 / uu;
            qreal dvv = 1 / vv;
            int stepx = (u < 0) ? -1 : 1;
            int stepy = (v < 0) ? -1 : 1;

            // the cell in the map that we need to check
            qreal px = playerPos.x();
            qreal py = playerPos.y();
            int mapx = static_cast<int>(px);
            int mapy = static_cast<int>(py);

            // the position and texture for the hit
            int texture = 0;
            qreal hitdist = 0.1;
            qreal texofs = 0;
            bool dark = false;

            // first hit at constant x and constant y lines
            qreal distx = (u > 0) ? (mapx + 1 - px) * duu : (px - mapx) * duu;
            qreal disty = (v > 0) ? (mapy + 1 - py) * dvv : (py - mapy) * dvv;

            // loop until we hit something
            while (texture <= 0) {
                if (distx > disty) {
                    // shorter distance to a hit in constant y line
                    hitdist = disty;
                    disty += dvv;
                    mapy += stepy;
                    texture = world_map[mapy][mapx];
                    if (texture > 0) {
                        dark = true;
                        if (stepy > 0) {
                            qreal ofs = px + u * (mapy - py) / v;
                            texofs = ofs - floor(ofs);
                        } else {
                            qreal ofs = px + u * (mapy + 1 - py) / v;
                            texofs = ofs - floor(ofs);
                        }
                    }
                } else {
                    // shorter distance to a hit in constant x line
                    hitdist = distx;
                    distx += duu;
                    mapx += stepx;
                    texture = world_map[mapy][mapx];
                    if (texture > 0) {
                        if (stepx > 0) {
                            qreal ofs = py + v * (mapx - px) / u;
                            texofs = ofs - floor(ofs);
                        } else {
                            qreal ofs = py + v * (mapx + 1 - px) / u;
                            texofs = ceil(ofs) - ofs;
                        }
                    }
                }
            }

            // get the texture, note that the texture image
            // has two textures horizontally, "normal" vs "dark"
            int col = static_cast<int>(texofs * TEXTURE_SIZE);
            col = qBound(0, col, TEXTURE_SIZE - 1);
            texture = (texture - 1) % textureCount;
            const QRgb *tex = texsrc + TEXTURE_BLOCK * texture * 2 +
                              (TEXTURE_SIZE * 2 * col);
            if (dark)
                tex += TEXTURE_SIZE;

            // start from the texture center (horizontally)
            int h = static_cast<int>(bufw / hitdist / 2);
            int dy = (TEXTURE_SIZE << 12) / h;
            int p1 = ((TEXTURE_SIZE / 2) << 12) - dy;
            int p2 = p1 + dy;

            // start from the screen center (vertically)
            // y1 will go up (decrease), y2 will go down (increase)
            int y1 = bufh / 2;
            int y2 = y1 + 1;
            QRgb *pixel1 = start + y1 * stride + ray;
            QRgb *pixel2 = pixel1 + stride;

            // map the texture to the sliver
            while (y1 >= 0 && y2 < bufh && p1 >= 0) {
                *pixel1 = tex[p1 >> 12];
                *pixel2 = tex[p2 >> 12];
                p1 -= dy;
                p2 += dy;
                --y1;
                ++y2;
                pixel1 -= stride;
                pixel2 += stride;
            }

            // ceiling and floor
            for (; pixel1 > start; pixel1 -= stride)
                *pixel1 = qRgb(0, 0, 0);
            for (; pixel2 < finish; pixel2 += stride)
                *pixel2 = qRgb(96, 96, 96);
        }

        update(QRect(QPoint(0, 0), bufferSize));
    }

protected:

    void resizeEvent(QResizeEvent*) {
        touchDevice = false;
        if (touchDevice) {
            if (width() < height()) {
                trackPad = QRect(0, height() / 2, width(), height() / 2);
                centerPad = QPoint(width() / 2, height() * 3 / 4);
                bufferSize = QSize(width(), height() / 2);
            } else {
                trackPad = QRect(width() / 2, 0, width() / 2, height());
                centerPad = QPoint(width() * 3 / 4, height() / 2);
                bufferSize = QSize(width() / 2, height());
            }
        } else {
            trackPad = QRect();
            bufferSize = size();
        }
        update();
   }

    void timerEvent(QTimerEvent*) {
        updatePlayer();
        render();
        showFps();
    }

    void paintEvent(QPaintEvent *event) {
        QPainter p(this);
        p.setCompositionMode(QPainter::CompositionMode_Source);

        p.drawImage(event->rect(), buffer, event->rect());

        if (touchDevice && event->rect().intersects(trackPad)) {
            p.fillRect(trackPad, Qt::white);
            p.setPen(QPen(QColor(224, 224, 224), 6));
            int rad = qMin(trackPad.width(), trackPad.height()) * 0.3;
            p.drawEllipse(centerPad, rad, rad);

            p.setPen(Qt::NoPen);
            p.setBrush(Qt::gray);

            QPolygon poly;
            poly << QPoint(-30, 0);
            poly << QPoint(0, -40);
            poly << QPoint(30, 0);

            p.translate(centerPad);
            for (int i = 0; i < 4; ++i) {
                p.rotate(90);
                p.translate(0, 20 - rad);
                p.drawPolygon(poly);
                p.translate(0, rad - 20);
            }
        }

        p.end();
    }

    void keyPressEvent(QKeyEvent *event) {
        event->accept();
        if (event->key() == Qt::Key_Left)
            angleDelta = 1.3 * M_PI;
        if (event->key() == Qt::Key_Right)
            angleDelta = -1.3 * M_PI;
        if (event->key() == Qt::Key_Up)
            moveDelta = 2.5;
        if (event->key() == Qt::Key_Down)
            moveDelta = -2.5;
    }

    void keyReleaseEvent(QKeyEvent *event) {
        event->accept();
        if (event->key() == Qt::Key_Left)
            angleDelta = (angleDelta > 0) ? 0 : angleDelta;
        if (event->key() == Qt::Key_Right)
            angleDelta = (angleDelta < 0) ? 0 : angleDelta;
        if (event->key() == Qt::Key_Up)
            moveDelta = (moveDelta > 0) ? 0 : moveDelta;
        if (event->key() == Qt::Key_Down)
            moveDelta = (moveDelta < 0) ? 0 : moveDelta;
    }

    void mousePressEvent(QMouseEvent *event) {
        qreal dx = centerPad.x() - event->position().toPoint().x();
        qreal dy = centerPad.y() - event->position().toPoint().y();
        angleDelta = dx * 2 * M_PI / width();
        moveDelta = dy * 10 / height();
    }

    void mouseMoveEvent(QMouseEvent *event) {
        qreal dx = centerPad.x() - event->position().toPoint().x();
        qreal dy = centerPad.y() - event->position().toPoint().y();
        angleDelta = dx * 2 * M_PI / width();
        moveDelta = dy * 10 / height();
    }

    void mouseReleaseEvent(QMouseEvent*) {
        angleDelta = 0;
        moveDelta = 0;
    }

private:
    QElapsedTimer watch;
    QBasicTimer ticker;
    QImage buffer;
    qreal angle;
    QPointF playerPos;
    qreal angleDelta;
    qreal moveDelta;
    QImage textureImg;
    int textureCount;
    bool touchDevice;
    QRect trackPad;
    QPoint centerPad;
    QSize bufferSize;
};

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    Raycasting w;
    w.setWindowTitle("Raycasting");
    w.resize(640, 480);
    w.show();

    return app.exec();
}
