#include <QtGui>
#include <QtCore>
#include <QtOpenGL>

#define TILE_SIZE 256
#define GRID_SIZE 16

int qt_system_depth = 32;

class WorkerThread : public QThread
{
    Q_OBJECT
public:
    WorkerThread(bool threaded)
    {
        if (threaded) {
            moveToThread(this);
            start();
        }
    }

    void run() {
        exec();
    }

public slots:
    void update(int tileX, int tileY, const QString &fileName)
    {
        QImageReader reader(fileName);
        QSize size= reader.size();

        int reduction = TILE_SIZE / 5;
        size.scale(QSize(TILE_SIZE - reduction, TILE_SIZE - reduction), Qt::KeepAspectRatio);

        // to handle very wide or very tall images which would collapse otherwise...
        if (size.isEmpty())
            size = QSize(TILE_SIZE - reduction, TILE_SIZE - reduction);

        reader.setScaledSize(size);
        QImage scaledImage = reader.read();

        QImage tile(TILE_SIZE, TILE_SIZE, qt_system_depth == 32 ? QImage::Format_RGB32 : QImage::Format_RGB16);
        // Don't clear the image as we're going to write all pixels anyway...

        QPainter p(&tile);
        QLinearGradient lg(0, 0, TILE_SIZE, TILE_SIZE);
        lg.setColorAt(0, QColor(240, 240, 240));
        lg.setColorAt(1, QColor(150, 150, 150));
        p.fillRect(tile.rect(), lg);
        p.setPen(QColor(0, 0, 0, 127));
        p.drawLine(0, tile.height() - 1, tile.width() - 1, tile.height() - 1);
        p.drawLine(tile.width() - 1, 0, tile.width() - 1, tile.height() - 1);
        QRectF imageRect(QPoint(TILE_SIZE / 2 - size.width() / 2,
                               TILE_SIZE / 2 - size.height() / 2),
                        size);
        p.drawImage(imageRect.topLeft(), scaledImage);

        // Draw a white border around the image to make it look like a photograph
        QRectF boxRect = imageRect.adjusted(-5, -5, 5, 5);
        QPainterPath boxPath;
        boxPath.addRect(imageRect);
        boxPath.addRect(boxRect);
        p.fillPath(boxPath, Qt::white);

        // Draw a curvy drop shadow, to give a slight feel of the paper curving

        qreal indent = 5;
        QPainterPath path;
        path.moveTo(boxRect.left() + indent, boxRect.bottom());
        path.lineTo(boxRect.left() + indent, boxRect.bottom() + indent * 2);
        path.quadTo(boxRect.center().x(), boxRect.bottom() + indent / 2,
                    boxRect.right() + indent * 2, boxRect.bottom() + indent * 2);
        path.quadTo(boxRect.right() + indent / 2, boxRect.center().y(),
                    boxRect.right() + indent, boxRect.top() + indent);
        path.lineTo(boxRect.right(), boxRect.top() + indent);
        path.lineTo(boxRect.right(), boxRect.bottom());
        path.closeSubpath();
        p.setRenderHint(QPainter::Antialiasing);
        p.fillPath(path, QColor(0, 0, 0, 63));
        p.end();

        emit tileDone(tileX, tileY, tile);
    }

signals:
    void tileDone(int tileX, int tileY, const QImage &image);
};

class MyClass : public QWidget
{
    Q_OBJECT
public:
    MyClass(WorkerThread *thread, const QList<QString> &imageFiles)
        :
        m_mouse_down(false),
        m_min_tileX(0),
        m_min_tileY(0),
        m_max_tileX(0),
        m_max_tileY(0)
    {
        m_worker_thread = thread;
        connect(m_worker_thread, SIGNAL(tileDone(int,int,QImage)), this, SLOT(tileDone(int,int,QImage)));
        m_grid_height = imageFiles.size() / GRID_SIZE + 1;
        m_grid_width = GRID_SIZE;

        m_count = imageFiles.size();
        m_grid = new Image[m_count];

        for (int i=0; i<m_count; ++i) {
            m_grid[i].name = imageFiles.at(i);
            m_grid[i].update_pending= false;
        }

        startTimer(1000/60);
    }

protected:

    void paintEvent(QPaintEvent *e)
    {
        QPainter p(this);

        for (int i=0; i<m_count; ++i) {
            int x = i % m_grid_width;
            int y = i / m_grid_width;
            QRect rect = tileRect(x, y).translated(-m_offset.toPoint());

            if (rect.intersects(e->rect())) {
                Image &img = m_grid[i];
                if (img.image.isNull())
                    requestUpdate(x, y);
                else {
                    img.opacity = qMin<qreal>(1, img.opacity + 0.1);
                    p.setOpacity(img.opacity);
                    p.drawImage(rect.topLeft(), img.image);
                }
            }
        }

        if (m_grid_overlay) {
            p.setOpacity(0.5);
            for (int i=0; i<m_count; ++i) {
                int x = i % m_grid_width;
                int y = i / m_grid_width;
                int ti = tileIndex(x, y);

                p.fillRect(x * 3, y * 3, 2, 2, m_grid[ti].image.isNull() ? Qt::red : Qt::blue);
            }
        }



        // Timing...
        static QTime time;
        static int frames = 0;
        static bool started = false;

        if (!started || time.elapsed() > 1000) {
            qreal fps = frames * 1000. / time.elapsed();
            if (fps == 0)
                m_current_fps = "counting fps...";
            else
                m_current_fps = QString::fromLatin1("%3 FPS").arg((int) qRound(fps));

            time.start();
            started = true;
            frames = 0;

        } else {
            ++frames;

            p.setOpacity(1);
            p.setFont(QFont("times", 30));
            p.fillRect(5, height() - 40, 250, 40, Qt::white);
            p.drawText(10, height() - 8, m_current_fps);
        }
    }

    void mousePressEvent(QMouseEvent *e) {
        m_mouse_pos = e->pos();
        m_mouse_down = true;
        m_speed = QPointF();
    }


    void mouseMoveEvent(QMouseEvent *e) {
        m_offset += (m_mouse_pos - e->pos());

        reactToPositionChange();
        if (m_mouse_down)
            m_speed = e->pos() - m_mouse_pos;

        m_mouse_pos = e->pos();
    }


    void mouseReleaseEvent(QMouseEvent *) {
        m_mouse_down = false;
    }

    void timerEvent(QTimerEvent *e) {
        if (!m_mouse_down) {
            m_speed *= 0.97;
            if (m_speed.manhattanLength() < 0.1)
                m_speed = QPointF(0, 0);
            m_offset -= m_speed;
            reactToPositionChange();
        }
        update();
    }

    void requestUpdate(int x, int y) {
        int ti = tileIndex(x, y);
        if (ti < 0)
            return;
        Image &item = m_grid[ti];
        if (item.update_pending || !item.image.isNull())
            return;
        item.update_pending = true;
        // threaded tiles start at 0 opacity, in-thread tiles at 1 as
        // they show up immediately
        item.opacity = m_worker_thread->thread() == QThread::currentThread() ? 1 : 0;
        QMetaObject::invokeMethod(m_worker_thread, "update", Qt::QueuedConnection, Q_ARG(int, x), Q_ARG(int, y), Q_ARG(QString, item.name));
    }

    QSize sizeHint() const { return QSize(500, 500); }

    int tileIndex(int gx, int gy) const {
        int ti = gy * m_grid_width + gx;
        if (ti >= m_count) return -1;
        return ti;
    }

    QImage tile(int gx, int gy) const {
        int ti = tileIndex(gx, gy);
        if (!m_grid || ti < 0)
            return QImage();
        return m_grid[ti].image;
    }

    void setTile(int tx, int ty, const QImage &image) {
        int ti = tileIndex(tx, ty);
        if (!m_grid || ti < 0)
            return;
        m_grid[ti].image = image;
    }

    QRect tileRect(int x, int y) const {
        return QRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
    }

    void reactToPositionChange() {

        if (m_offset.x() < 0)
            m_offset.setX(0);
        if (m_offset.x() > m_grid_width * TILE_SIZE - width())
            m_offset.setX(m_grid_width * TILE_SIZE - width() - 1);
        if (m_offset.y() < 0)
            m_offset.setY(0);
        if (m_offset.y() > m_grid_height * TILE_SIZE - height())
            m_offset.setY(m_grid_height * TILE_SIZE - height() - 1);

        int minTileX = int(m_offset.x()) / TILE_SIZE - 2;
        int minTileY = int(m_offset.y()) / TILE_SIZE - 2;

        int maxTileX = minTileX + width() / TILE_SIZE + 5;
        int maxTileY = minTileY + height() / TILE_SIZE + 5;

        minTileX = qMax(minTileX, 0);
        maxTileX = qMin(maxTileX, m_grid_width - 1);

        minTileY = qMax(minTileY, 0);
        maxTileY = qMin(maxTileY, m_grid_height - 1);

        if (m_discard) {
            // Discard on the left side
            if (minTileX > m_min_tileX) {
                for (int x=m_min_tileX; x<minTileX; ++x) {
                    for (int y=m_min_tileY; y<=m_max_tileY; ++y) {
                        setTile(x, y, QImage());
                    }
                }
            }

            // Discard on the right side
            if (maxTileX < m_max_tileX) {
                for (int x=maxTileX+1; x<=m_max_tileX; ++x) {
                    for (int y=m_min_tileY; y<=m_max_tileY; ++y) {
                        setTile(x, y, QImage());
                    }
                }
            }

            // Discard on the top
            if (minTileY > m_min_tileY) {
                for (int y=m_min_tileY; y<minTileY; ++y) {
                    for (int x=m_min_tileX; x<=m_max_tileX; ++x) {
                        setTile(x, y, QImage());
                    }
                }
            }

            // Discard on the bottom
            if (maxTileY < m_max_tileY) {
                for (int y=maxTileY+1; y<=m_max_tileY; ++y) {
                    for (int x=m_min_tileX; x<=m_max_tileX; ++x) {
                        setTile(x, y, QImage());
                    }
                }
            }
        }

        m_min_tileX = minTileX;
        m_min_tileY = minTileY;
        m_max_tileX = maxTileX;
        m_max_tileY = maxTileY;
    }

public slots:
    void tileDone(int tx, int ty, const QImage &image)
    {
        int ti = tileIndex(tx, ty);
        if (ti < 0 || !m_grid)
            return;

        m_grid[ti].image = image;
        m_grid[ti].update_pending = false;
    }

public:

    QPointF m_speed;
    QPointF m_offset;
    QPoint m_mouse_pos;
    bool m_mouse_down;

    bool m_grid_overlay;
    bool m_discard;

    int m_grid_width;
    int m_grid_height;
    int m_count;

    struct Image {
        QImage image;
        bool update_pending;
        QString name;
        qreal opacity;
    };

    Image *m_grid;

    WorkerThread *m_worker_thread;

    int m_min_tileX, m_min_tileY;
    int m_max_tileX, m_max_tileY;

    int m_timer_id;
    QString m_current_fps;
};

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    bool gridOverlay = false;
    bool discard = true;
    bool use_thread = true;

    QList<QString> files;

    QStringList args = app.arguments();
    for (int i=0; i<args.size(); ++i) {
        if (args.at(i) == "-grid-overlay")
            gridOverlay = true;
        else if (args.at(i) == "-no-discard")
            discard = false;
        else if (args.at(i) == "-no-thread")
            use_thread = false;
        else if (i == args.size() - 1) {
            QDir dir(args.at(i));
            QStringList filter;
            foreach (const QByteArray &ext, QImageReader::supportedImageFormats())
                filter << QLatin1String("*.") + ext;
            QStringList localFiles = dir.entryList(filter);
            foreach (const QString &name, localFiles)
                files << dir.absoluteFilePath(name);
            if (files.size() == 0) {
                printf("no images in folder '%s'\n", qPrintable(dir.dirName()));
                return 0;
            }
        }
    }

    if (args.size() == 0) {
        printf("Please specify image directory...\n");
        return 0;
    }

    {
        QWidget dummy;
        qt_system_depth = dummy.depth();
    }

    printf("System depth: %d\n", qt_system_depth);
    printf("%s (-grid-overlay)\n", gridOverlay ? "using grid overlay" : "not using grid overlay");
    printf("%s (-no-discard)\n", discard ? "discarding tiles" : "not discarding tiles");
    printf("%s (-no-thread)\n", use_thread ? "using a thread" : "not using a thread");

    WorkerThread thread(use_thread);

    MyClass cl(&thread, files);
    cl.m_grid_overlay = gridOverlay;
    cl.m_discard = discard;
    cl.showMaximized();

    int ret = app.exec();
    return ret;
}

#include "threaded_tile_generation.moc"

