#include "imagescrollareagl.h"
#include <QOpenGLFunctions>
#include <QDebug>
#include <QKeyEvent>
#include <QOpenGLDebugLogger>
#include <QOpenGLPixelTransferOptions>
#include <QOpenGLFramebufferObject>
#include <QGridLayout>

struct RawImageType
{
    QOpenGLTexture::PixelFormat pixelFormat;
    QOpenGLTexture::TextureFormat textureFormat;
    QOpenGLTexture::PixelType dataType;
    bool bw;
};

const RawImageType rawImageTypes[] = {
    {QOpenGLTexture::Red, QOpenGLTexture::R8_UNorm, QOpenGLTexture::UInt8, true},
    {QOpenGLTexture::Red, QOpenGLTexture::R16_UNorm, QOpenGLTexture::UInt16, true},
    {QOpenGLTexture::Red, QOpenGLTexture::R32F, QOpenGLTexture::Float32, true},
    {QOpenGLTexture::RGB, QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::UInt8, false},
    {QOpenGLTexture::BGRA,QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::UInt8, false},
    {QOpenGLTexture::RGB, QOpenGLTexture::RGB16_UNorm, QOpenGLTexture::UInt16, false},
    {QOpenGLTexture::RGB, QOpenGLTexture::RGB32F, QOpenGLTexture::Float32, false}
};

void setScrollRange(QScrollBar *scrollBar, int newRange)
{
    int page = scrollBar->pageStep();
    int pos = scrollBar->value() + page/2;
    int range = scrollBar->maximum() + page;
    float relPos = (float)pos/(float)range;

    if(page >= newRange)
        scrollBar->hide();
    else
        scrollBar->show();

    scrollBar->setRange(0, newRange - page);
    scrollBar->setValue(relPos*newRange - page/2);
}

ImageWidget::ImageWidget(QWidget *parent) : QOpenGLWidget(parent)
{
    setFocusPolicy(Qt::ClickFocus);
    m_range = UINT16_MAX;
    m_low = 0;
    m_mid = 0.5;
    m_high = 1;
    m_dx = m_dy = 0;
    m_scale = 1.0f;
    m_blockRepaint = false;
    m_range = UINT16_MAX;
    m_imgWidth = m_imgHeight = -1;
    m_superpixel = m_invert = false;
}

ImageWidget::~ImageWidget()
{
    makeCurrent();
}

void ImageWidget::setImage(const RawImage *image)
{
    if(image == nullptr)return;

    m_imgWidth = image->width();
    m_imgHeight = image->height();

    const RawImageType &rawImageType = rawImageTypes[image->type()];

    m_image->destroy();
    m_image->setFormat(rawImageType.textureFormat);
    m_image->setSize(image->width(), image->height());
    m_image->setMipLevels([&](){ int c = 0; int s = std::min(m_imgWidth, m_imgHeight); while(s>>=1)c++; return c; }());
    m_image->allocateStorage();
    m_image->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);
    m_image->setWrapMode(QOpenGLTexture::ClampToEdge);
    m_image->setBorderColor(0, 0, 0, 0);
    m_image->setData(0, rawImageType.pixelFormat, rawImageType.dataType, image->data(), m_transferOptions.get());
    m_image->setLevelOfDetailRange(m_superpixel ? 1 : 0, m_image->mipMaxLevel());
    m_image->generateMipMaps();
    m_bwImg = rawImageType.bw;
    update();
}

void ImageWidget::setImage(const QPixmap &pixmap)
{
    QImage img = pixmap.toImage();

    m_imgWidth = pixmap.width();
    m_imgHeight = pixmap.height();

    m_image->destroy();
    m_image->setData(img);
    m_image->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);
    m_image->setWrapMode(QOpenGLTexture::ClampToBorder);
    m_image->setBorderColor(0, 0, 0, 0);
    m_bwImg = false;
    update();
}

void ImageWidget::setScale(float scale)
{
    m_scale = scale;
    update();
}

void ImageWidget::blockRepaint(bool block)
{
    m_blockRepaint = block;
    if(!block)update();
}

void ImageWidget::setMTFParams(float low, float mid, float high)
{
    m_low = low;
    m_mid = mid;
    m_high = high;
    update();
}

void ImageWidget::setOffset(int dx, int dy)
{
    m_dx = dx;
    m_dy = dy;
    update();
}

void ImageWidget::superPixel(bool enable)
{
    m_superpixel = enable;
    m_image->setLevelOfDetailRange(enable ? 1 : 0, m_image->mipMaxLevel());
    update();
}

void ImageWidget::invert(bool enable)
{
    m_invert = enable;
    update();
}

QImage ImageWidget::renderToImage()
{
    if(m_imgWidth < 0)return QImage();
    makeCurrent();
    QOpenGLFramebufferObject fbo(m_imgWidth, m_imgHeight);
    fbo.bind();

    f->glViewport(0, 0, m_imgWidth, m_imgHeight);

    m_program->bind();
    m_program->setUniformValue("viewport", (float)m_imgWidth, (float)m_imgHeight);
    m_program->setUniformValue("offset", 0.0f, 0.0f);
    m_program->setUniformValue("zoom", 1.0f);

    m_image->bind(0);
    f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    fbo.bindDefault();
    return fbo.toImage(true);
}

void ImageWidget::paintGL()
{
    if(m_blockRepaint)return;

    float dx = m_dx;
    float dy = m_dy;
    if(width() > m_image->width()*m_scale)
        dx = -width()*0.5f + m_image->width()*m_scale*0.5f;
    if(height() > m_image->height()*m_scale)
        dy = -height()*0.5f + m_image->height()*m_scale*0.5f;

    m_program->bind();
    m_program->setUniformValue("viewport", (float)width(), (float)height());
    m_program->setUniformValue("offset", dx, dy);
    m_program->setUniformValue("mtf_param", m_low, m_mid, m_high);
    m_program->setUniformValue("zoom", 1.0f/m_scale);
    m_program->setUniformValue("bw", m_bwImg);
    m_program->setUniformValue("invert", m_invert);

    m_image->bind(0);
    f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

void ImageWidget::resizeGL(int w, int h)
{
    m_width = w;
    m_height = h;
    f->glViewport(0, 0, w, h);
}

void ImageWidget::initializeGL()
{
    f = context()->functions();
    f->glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
    QOpenGLFunctions_3_2_Core *f3 = context()->versionFunctions<QOpenGLFunctions_3_2_Core>();

    m_vao = std::unique_ptr<QOpenGLVertexArrayObject>(new QOpenGLVertexArrayObject);
    m_vao->create();
    m_vao->bind();

    QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(this);
    logger->initialize();
    logger->startLogging();
    connect(logger, &QOpenGLDebugLogger::messageLogged, [](const QOpenGLDebugMessage &message)
        {
            qDebug() << message;
        });

    qDebug() << (char*)f->glGetString(GL_VENDOR);
    qDebug() << (char*)f->glGetString(GL_RENDERER);
    qDebug() << (char*)f->glGetString(GL_VERSION);

    qDebug() << context()->format();

    // each vertex is  x,y 2D position and s,t texture coordinates
    float vertexs[] = {-1.0f, -1.0f, 0.0f, 1.0f,
                        1.0f, -1.0f, 1.0f, 1.0f,
                       -1.0f,  1.0f, 0.0f, 0.0f,
                        1.0f,  1.0f, 1.0f, 0.0f,};
    m_buffer = std::unique_ptr<QOpenGLBuffer>(new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer));
    m_buffer->setUsagePattern(QOpenGLBuffer::StaticDraw);
    m_buffer->create();
    m_buffer->bind();
    m_buffer->allocate(vertexs, sizeof(vertexs));
    f->glVertexAttribPointer(0, 2, GL_FLOAT, false, sizeof(float)*4, 0);

    m_program = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
    m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/image.vert");
    m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/image.frag");
    if(f3)f3->glBindFragDataLocation(m_program->programId(), 0, "color");
    if(!m_program->link())
    {
        qDebug() << "Link failed" << m_program->log();
    }

    m_program->bind();
    m_program->enableAttributeArray("qt_Vertex");
    m_program->setAttributeBuffer("qt_Vertex", GL_FLOAT, 0, 2, sizeof(float)*4);
    m_program->enableAttributeArray("qt_MultiTexCoord0");
    m_program->setAttributeBuffer("qt_MultiTexCoord0", GL_FLOAT, sizeof(float)*2, 2, sizeof(float)*4);
    m_program->setUniformValue("qt_Texture0", (GLuint)0);
    m_program->setUniformValue("scale", 1.0f, 0.0f);

    m_image = std::unique_ptr<QOpenGLTexture>(new QOpenGLTexture(QOpenGLTexture::Target2D));
    m_image->setFormat(QOpenGLTexture::RGB8U);
    m_image->allocateStorage();
    m_image->bind(0);
    m_image->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
    m_image->setMagnificationFilter(QOpenGLTexture::Linear);

    m_transferOptions = std::unique_ptr<QOpenGLPixelTransferOptions>(new QOpenGLPixelTransferOptions);
    m_transferOptions->setAlignment(1);
}

ImageScrollAreaGL::ImageScrollAreaGL(QWidget *parent) : QWidget(parent)
{
    QGridLayout *layout = new QGridLayout(this);
    setLayout(layout);

    m_imageWidget = new ImageWidget(this);

    m_verticalScrollBar = new QScrollBar(Qt::Vertical, this);
    m_horizontalScrollBar = new QScrollBar(Qt::Horizontal, this);
    m_scale = 1.0f;
    m_bestFit = false;

    layout->setSpacing(0);
    layout->addWidget(m_imageWidget, 0, 0);
    layout->addWidget(m_verticalScrollBar, 0, 1);
    layout->addWidget(m_horizontalScrollBar, 1, 0);

    connect(m_verticalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(scrollEvent()));
    connect(m_horizontalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(scrollEvent()));
}

ImageScrollAreaGL::~ImageScrollAreaGL()
{

}

void ImageScrollAreaGL::setImage(RawImage *image)
{
    m_imageWidget->setImage(image);
    m_imgWidth = image->width();
    m_imgHeight = image->height();
    if(m_bestFit)bestFit();
    updateScrollbars();
}

void ImageScrollAreaGL::setImage(const QPixmap &pixmap)
{
    m_imageWidget->setImage(pixmap);
    m_imgWidth = pixmap.width();
    m_imgHeight = pixmap.height();
    if(m_bestFit)bestFit();
    updateScrollbars();
}

ImageWidget *ImageScrollAreaGL::imageWidget()
{
    return m_imageWidget;
}

void ImageScrollAreaGL::updateScrollbars(bool zoom)
{
    if(zoom)
    {
        setScrollRange(m_verticalScrollBar, m_imgHeight*m_scale);
        setScrollRange(m_horizontalScrollBar, m_imgWidth*m_scale);
    }
    else
    {
        m_verticalScrollBar->setRange(0, m_imgHeight*m_scale - m_verticalScrollBar->pageStep());
        m_horizontalScrollBar->setRange(0, m_imgWidth*m_scale - m_horizontalScrollBar->pageStep());
    }
}

void ImageScrollAreaGL::resizeEvent(QResizeEvent *event)
{
    QWidget::resizeEvent(event);
    m_verticalScrollBar->setPageStep(m_imageWidget->height());
    m_horizontalScrollBar->setPageStep(m_imageWidget->width());
    updateScrollbars();
}

void ImageScrollAreaGL::mouseMoveEvent(QMouseEvent *event)
{
    QPoint delta = m_lastPos - event->pos();
    m_horizontalScrollBar->setValue(m_horizontalScrollBar->value() + delta.x());
    m_verticalScrollBar->setValue(m_verticalScrollBar->value() + delta.y());
    m_lastPos = event->pos();
}

void ImageScrollAreaGL::mousePressEvent(QMouseEvent *event)
{
    m_lastPos = event->pos();
}

void ImageScrollAreaGL::wheelEvent(QWheelEvent *event)
{
    m_bestFit = false;
    if(event->angleDelta().y() != 0)
        zoom(event->angleDelta().y() / 1200.0f);
}

void ImageScrollAreaGL::zoom(float delta)
{
    if((m_scale >= 8.0f && delta > 0) || (m_scale <= 0.1f && delta < 0))return;

    m_scale += delta;
    m_imageWidget->blockRepaint(true);
    m_imageWidget->setScale(m_scale);
    updateScrollbars(true);
    m_imageWidget->blockRepaint(false);
}

void ImageScrollAreaGL::zoomIn()
{
    zoom(0.1f);
    m_bestFit = false;
}

void ImageScrollAreaGL::zoomOut()
{
    zoom(-0.1f);
    m_bestFit = false;
}

void ImageScrollAreaGL::bestFit()
{
    m_bestFit = true;
    m_scale = std::min((float)m_imageWidget->width()/m_imgWidth, (float)m_imageWidget->height()/m_imgHeight);
    zoom(0.0f);
}

void ImageScrollAreaGL::oneToOne()
{
    m_scale = 1.0f;
    zoom(0.0f);
    m_bestFit = false;
}

void ImageScrollAreaGL::scrollEvent()
{
    m_imageWidget->setOffset(m_horizontalScrollBar->value(), m_verticalScrollBar->value());
}
