/*
    Stream Client
    Copyright (C) 2022 Jasem Mutlaq (mutlaqja@ikarustech.com)

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include "client.h"

#include <basedevice.h>
#include <QImage>
#include <QPixmap>
#include <QBuffer>
#include <QFile>
#include <QPainter>

#include <cstring>
#include <fstream>
#include <iostream>
#include <functional>
#include <memory>

#include <stdio.h>
#include <jpeglib.h>
#include <setjmp.h>
#include <vector>

// Error handler
struct my_error_mgr
{
    struct jpeg_error_mgr pub;
    jmp_buf setjmp_buffer;
};

void my_error_exit(j_common_ptr cinfo)
{
    my_error_mgr* err = (my_error_mgr*)cinfo->err;
    (*cinfo->err->error_exit)(cinfo);
    longjmp(err->setjmp_buffer, 1);
}

template<typename T>
class array_view
{
        T* ptr_;
        std::size_t len_;
    public:
        array_view(T* ptr, std::size_t len) noexcept: ptr_{ptr}, len_{len} {}

        T &operator[](int i) noexcept
        {
            return ptr_[i];
        }
        T const &operator[](int i) const noexcept
        {
            return ptr_[i];
        }
        auto size() const noexcept
        {
            return len_;
        }

        auto begin() noexcept
        {
            return ptr_;
        }
        auto end() noexcept
        {
            return ptr_ + len_;
        }
};

/**************************************************************************************
**
***************************************************************************************/
Client::Client(const QString &name) : m_Name(name)
{
    setServer("localhost", 7624);
}

/**************************************************************************************
**
***************************************************************************************/
bool Client::createHistogram(uint8_t *buffer, uint32_t size, std::vector<uint32_t> &histogram, uint8_t &channels)
{
    struct jpeg_decompress_struct cinfo;
    struct my_error_mgr jerr;

    cinfo.err = jpeg_std_error(&jerr.pub);
    jerr.pub.error_exit = my_error_exit;

    if (setjmp(jerr.setjmp_buffer))
    {
        jpeg_destroy_decompress(&cinfo);
        return false;
    }

    jpeg_create_decompress(&cinfo);

    // Use the fast integer DCT and skip fancy upsampling — significant speedup
    // with negligible quality loss for histogram purposes.
    cinfo.dct_method            = JDCT_IFAST;
    cinfo.do_fancy_upsampling   = FALSE;
    cinfo.do_block_smoothing    = FALSE;

    jpeg_mem_src(&cinfo, buffer, size);
    jpeg_read_header(&cinfo, TRUE);
    if (jpeg_start_decompress(&cinfo) == false)
        return false;

    channels = cinfo.num_components;
    const int imageWidth  = static_cast<int>(cinfo.image_width);
    const int imageHeight = static_cast<int>(cinfo.image_height);
    const int pictureSize = imageWidth * imageHeight;
    const int lineWidth   = imageWidth * channels;

    // Proper integer ceiling division: skip every Nth pixel group so we sample
    // at most ~50 000 pixels regardless of image resolution.
    int downsample = 1;
    if (pictureSize > 50000)
        downsample = (pictureSize + 49999) / 50000;

    const int skip = downsample * channels;

    // Use uint32_t bins — a 12 MP image can have millions of pixels per bin,
    // which would silently overflow a uint8_t bucket.
    histogram.assign(256 * channels, 0);

    // Allocate the scanline buffer once outside the loop instead of once per row.
    std::vector<JSAMPLE> rowBuffer(lineWidth);
    JSAMPLE* row_pointer[1] = { rowBuffer.data() };

    while (cinfo.output_scanline < cinfo.output_height)
    {
        jpeg_read_scanlines(&cinfo, row_pointer, 1);

        if (channels == 1)
        {
            for (int x = 0; x < lineWidth; x += skip)
                histogram[row_pointer[0][x]]++;
        }
        else
        {
            for (int x = 0; x + 2 < lineWidth; x += skip)
            {
                histogram[row_pointer[0][x]]++;
                histogram[256 + row_pointer[0][x + 1]]++;
                histogram[512 + row_pointer[0][x + 2]]++;
            }
        }
    }

    jpeg_finish_decompress(&cinfo);
    jpeg_destroy_decompress(&cinfo);

    return true;
}

/**************************************************************************************
**
***************************************************************************************/
bool Client::drawHistogram(uint8_t *buffer, uint32_t size, std::string &histogram)
{
    std::vector<uint32_t> histogramArray;
    uint8_t channels = 1;
    const int height    = 100;
    const int width     = 300;
    const double binWidth = width / 256.0;

    if (!createHistogram(buffer, size, histogramArray, channels))
        return false;

    if (channels == 1)
    {
        double maxCount = static_cast<double>(*std::max_element(histogramArray.begin(), histogramArray.end()));
        if (maxCount == 0)
            return false;

        QImage image(width, height, QImage::Format_Grayscale8);
        image.fill(QColor(0, 0, 0, 255));
        QPainter painter(&image);
        // No antialiasing needed for a histogram — saves CPU.
        painter.setPen(Qt::white);

        std::vector<int> normalized(256);
        for (int i = 0; i < 256; i++)
            normalized[i] = height - static_cast<int>(histogramArray[i] / maxCount * height);

        for (int i = 0; i < 255; ++i)
            painter.drawLine(static_cast<int>(i * binWidth),       normalized[i],
                             static_cast<int>((i + 1) * binWidth), normalized[i + 1]);

        QByteArray jpegData;
        QBuffer jpegBuffer(&jpegData);
        jpegBuffer.open(QIODevice::WriteOnly);
        image.save(&jpegBuffer, "JPEG");
        histogram = jpegData.toStdString();
        return true;
    }
    else if (channels == 3)
    {
        double maxR = static_cast<double>(*std::max_element(histogramArray.begin(),       histogramArray.begin() + 256));
        double maxG = static_cast<double>(*std::max_element(histogramArray.begin() + 256, histogramArray.begin() + 512));
        double maxB = static_cast<double>(*std::max_element(histogramArray.begin() + 512, histogramArray.end()));

        if (maxR == 0 || maxG == 0 || maxB == 0)
            return false;

        std::vector<int> r(256), g(256), b(256);
        for (int i = 0; i < 256; i++)
            r[i] = height - static_cast<int>(histogramArray[i]       / maxR * height);
        for (int i = 0; i < 256; i++)
            g[i] = height - static_cast<int>(histogramArray[i + 256] / maxG * height);
        for (int i = 0; i < 256; i++)
            b[i] = height - static_cast<int>(histogramArray[i + 512] / maxB * height);

        QImage image(width, height, QImage::Format_RGB888);
        image.fill(QColor(0, 0, 0, 255));
        QPainter painter(&image);
        // No antialiasing needed for a histogram — saves CPU.

        painter.setPen(Qt::red);
        for (int i = 0; i < 255; ++i)
            painter.drawLine(static_cast<int>(i * binWidth),       r[i],
                             static_cast<int>((i + 1) * binWidth), r[i + 1]);

        painter.setPen(Qt::green);
        for (int i = 0; i < 255; ++i)
            painter.drawLine(static_cast<int>(i * binWidth),       g[i],
                             static_cast<int>((i + 1) * binWidth), g[i + 1]);

        painter.setPen(Qt::blue);
        for (int i = 0; i < 255; ++i)
            painter.drawLine(static_cast<int>(i * binWidth),       b[i],
                             static_cast<int>((i + 1) * binWidth), b[i + 1]);

        QByteArray jpegData;
        QBuffer jpegBuffer(&jpegData);
        jpegBuffer.open(QIODevice::WriteOnly);
        image.save(&jpegBuffer, "JPEG");
        histogram = jpegData.toStdString();
        return true;
    }

    return false;
}

/**************************************************************************************
**
***************************************************************************************/
bool Client::start()
{
    const auto device = m_Name.toLatin1().constData();

    if (connectServer() == false)
        return false;

    setBLOBMode(B_ONLY, device, "CCD1");

    enableDirectBlobAccess(device, "CCD1");

    // wait for the availability of the device
    watchDevice(device, [this](INDI::BaseDevice device)
    {
        m_Camera = device;

        // wait for the availability of the "CCD1" property
        device.watchProperty("CCD1", [this](INDI::PropertyBlob property)
        {
            m_Streamer.start(m_Port);
            //call function if property changed
            property.onUpdate([this, property]()
            {
                array_view<uchar> buffer(static_cast<uchar *>(property[0].getBlob()), property[0].getBlobLen());
                m_Streamer.publish("/jpg", std::string(buffer.begin(), buffer.end()));

                // Throttle histogram computation — there is no benefit in computing
                // a new histogram faster than the client can display it, and at
                // 30–60+ fps the decode+draw cost would dominate CPU usage.
                const auto now     = std::chrono::steady_clock::now();
                const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastHistogramTime).count();
                if (elapsed >= (1000 / HISTOGRAM_MAX_FPS))
                {
                    std::string histogramData;
                    if (drawHistogram(reinterpret_cast<uint8_t * >(property[0].getBlob()),
                                      property[0].getBlobLen(), histogramData))
                    {
                        m_Streamer.publish("/histogram", histogramData);
                    }
                    m_lastHistogramTime = now;
                }
            });
        });
    });

    return true;
}

/**************************************************************************************
**
***************************************************************************************/
bool Client::stop()
{
    return false;
}
