/*
    SPDX-FileCopyrightText: 2004 Jasem Mutlaq <mutlaqja@ikarustech.com>

    SPDX-License-Identifier: GPL-2.0-or-later

    Some code fragments were adapted from Peter Kirchgessner's FITS plugin
    SPDX-FileCopyrightText: Peter Kirchgessner <http://members.aol.com/pkirchg>
*/

#include "fitsdata.h"
#include "fpack.h"
#include "robuststatistics.h"
#include "stretch.h"


#include <QImage>
#include <QtConcurrent>
#include <QImageReader>
#include <QRegularExpression>
#include <QDir>
#include <QUuid>
#include <QtGlobal>

#include <wcshdr.h>
#include <wcsfix.h>

#include <libraw/libraw.h>

#include <libxisf.h>

#include <cfloat>
#include <cmath>

#define ZOOM_DEFAULT   100.0
#define ZOOM_MIN       10
#define ZOOM_MAX       400
#define ZOOM_LOW_INCR  10
#define ZOOM_HIGH_INCR 50

QString getTemporaryPath()
{
    return "/tmp";
}

const QStringList RAWFormats = { "cr2", "cr3", "crw", "nef", "raf", "dng", "arw", "orf" };

bool FITSData::readableFilename(const QString &filename)
{
    QFileInfo info(filename);
    QString extension = info.completeSuffix().toLower();
    return extension.contains("fit") || extension.contains("fz") ||
           extension.contains("xisf") ||
           QImageReader::supportedImageFormats().contains(extension.toLatin1()) ||
           RAWFormats.contains(extension);
}

FITSData::FITSData(FITSMode fitsMode): m_Mode(fitsMode)
{
    qRegisterMetaType<FITSMode>("FITSMode");

    debayerParams.method  = DC1394_BAYER_METHOD_NEAREST;
    debayerParams.filter  = DC1394_COLOR_FILTER_RGGB;
    debayerParams.offsetX = debayerParams.offsetY = 0;

    // Reserve 3 channels
    m_CumulativeFrequency.resize(3);
    m_HistogramBinWidth.resize(3);
    m_HistogramFrequency.resize(3);
    m_HistogramIntensity.resize(3);
}

FITSData::FITSData(const QSharedPointer<FITSData> &other)
{
    qRegisterMetaType<FITSMode>("FITSMode");

    debayerParams.method  = DC1394_BAYER_METHOD_NEAREST;
    debayerParams.filter  = DC1394_COLOR_FILTER_RGGB;
    debayerParams.offsetX = debayerParams.offsetY = 0;

    m_TemporaryDataFile.setFileTemplate("fits_memory_XXXXXX");

    this->m_Mode = other->m_Mode;
    this->m_Statistics.channels = other->m_Statistics.channels;
    memcpy(&m_Statistics, &(other->m_Statistics), sizeof(m_Statistics));
    m_ImageBuffer = new uint8_t[m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel];
    memcpy(m_ImageBuffer, other->m_ImageBuffer,
           m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel);
}

FITSData::~FITSData()
{
    int status = 0;

    clearImageBuffers();

#ifdef HAVE_WCSLIB
    if (m_WCSHandle != nullptr)
    {
        wcsvfree(&m_nwcs, &m_WCSHandle);
        m_WCSHandle = nullptr;
        m_nwcs = 0;
    }
#endif

    if (starCenters.count() > 0)
        qDeleteAll(starCenters);
    starCenters.clear();

    // if (fptr != nullptr)
    // {
    //     fits_flush_file(fptr, &status);
    //     fits_close_file(fptr, &status);
    //     free(m_PackBuffer);
    //     m_PackBuffer = nullptr;
    //     fptr = nullptr;
    // }
}

void FITSData::loadCommon(const QString &inFilename)
{
    int status = 0;
    qDeleteAll(starCenters);
    starCenters.clear();

    if (fptr != nullptr)
    {
        fits_flush_file(fptr, &status);
        fits_close_file(fptr, &status);
        free(m_PackBuffer);
        m_PackBuffer = nullptr;
        fptr = nullptr;
    }

    m_Filename = inFilename;
}

bool FITSData::loadFromBuffer(const QByteArray &buffer, const QString &extension, const QString &inFilename)
{
    loadCommon(inFilename);
    m_Extension = extension;
    return privateLoad(buffer);
}

QFuture<bool> FITSData::loadFromFile(const QString &inFilename)
{
    loadCommon(inFilename);
    QFileInfo info(m_Filename);
    m_Extension = info.completeSuffix().toLower();
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    return QtConcurrent::run(this, &FITSData::privateLoad, QByteArray());
#else
    return QtConcurrent::run(&FITSData::privateLoad, this, QByteArray());
#endif
}

namespace
{
// Common code for reporting fits read errors. Always returns false.
QString fitsErrorToString(int status)
{
    char error_status[512] = {0};
    fits_report_error(stderr, status);
    fits_get_errstatus(status, error_status);
    QString message = error_status;
    return message;
}
}

bool FITSData::privateLoad(const QByteArray &buffer)
{
    m_isTemporary = m_Filename.startsWith("/tmp");
    cacheHFR = -1;
    cacheEccentricity = -1;

    if (m_Extension.contains("fit") || m_Extension.contains("fz"))
        return loadFITSImage(buffer);
    if (m_Extension.contains("xisf"))
        return loadXISFImage(buffer);
    if (QImageReader::supportedImageFormats().contains(m_Extension.toLatin1()))
        return loadCanonicalImage(buffer);
    else if (RAWFormats.contains(m_Extension))
        return loadRAWImage(buffer);

    return false;
}

bool FITSData::loadFITSImage(const QByteArray &buffer, const bool isCompressed)
{
    int status = 0, anynull = 0;
    long naxes[3];

    m_HistogramConstructed = false;

    if (m_Extension.contains(".fz") || isCompressed)
    {
        fpstate fpvar;
        fp_init (&fpvar);
        bool rc = false;

        if (buffer.isEmpty())
        {
            // Store so we don't lose.
            m_compressedFilename = m_Filename;

            QString uncompressedFile = QDir::tempPath() + QString("/%1").arg(QUuid::createUuid().toString().remove(
                                           QRegularExpression("[-{}]")));

            rc = fp_unpack_file_to_fits(m_Filename.toLocal8Bit().data(), &fptr, fpvar) == 0;
            if (rc)
            {
                m_Filename = uncompressedFile;
            }
        }
        else
        {
            size_t m_PackBufferSize = 100000;
            free(m_PackBuffer);
            m_PackBuffer = (uint8_t *)malloc(m_PackBufferSize);
            rc = fp_unpack_data_to_data(buffer.data(), buffer.size(), &m_PackBuffer, &m_PackBufferSize, fpvar) == 0;

            if (rc)
            {
                void *data = reinterpret_cast<void *>(m_PackBuffer);
                if (fits_open_memfile(&fptr, m_Filename.toLocal8Bit().data(), READONLY, &data, &m_PackBufferSize, 0,
                                      nullptr, &status))
                {
                    return false;
                }

                m_Statistics.size = m_PackBufferSize;
            }
            //rc = fp_unpack_data_to_fits(buffer.data(), buffer.size(), &fptr, fpvar) == 0;
        }

        if (rc == false)
        {
            free(m_PackBuffer);
            m_PackBuffer = nullptr;
            return false;
        }

        m_isTemporary = true;
        m_isCompressed = true;
        m_Statistics.size = fptr->Fptr->logfilesize;

    }
    else if (buffer.isEmpty())
    {
        // Use open diskfile as it does not use extended file names which has problems opening
        // files with [ ] or ( ) in their names.
        if (fits_open_diskfile(&fptr, m_Filename.toLocal8Bit(), READONLY, &status))
        {
        }

        m_Statistics.size = QFile(m_Filename).size();
    }
    else
    {
        // Read the FITS file from a memory buffer.
        void *temp_buffer = const_cast<void *>(reinterpret_cast<const void *>(buffer.data()));
        size_t temp_size = buffer.size();
        if (fits_open_memfile(&fptr, m_Filename.toLocal8Bit().data(), READONLY,
                              &temp_buffer, &temp_size, 0, nullptr, &status))
        {
            return false;
        }

        m_Statistics.size = temp_size;
    }

    if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status))
    {

        free(m_PackBuffer);
        m_PackBuffer = nullptr;
    }

    if (fits_get_img_param(fptr, 3, &m_FITSBITPIX, &(m_Statistics.ndim), naxes, &status))
    {
        free(m_PackBuffer);
        m_PackBuffer = nullptr;
        return false;
    }

    // Reload if it is transparently compressed.
    if ((fits_is_compressed_image(fptr, &status) || m_Statistics.ndim <= 0) && !isCompressed)
    {
        loadCommon(m_Filename);
        return loadFITSImage(buffer, true);
    }

    if (m_Statistics.ndim < 2)
    {
        free(m_PackBuffer);
        m_PackBuffer = nullptr;
        return false;
    }

    switch (m_FITSBITPIX)
    {
        case BYTE_IMG:
            m_Statistics.dataType      = TBYTE;
            m_Statistics.bytesPerPixel = sizeof(uint8_t);
            break;
        case SHORT_IMG:
            // Read SHORT image as USHORT
            m_FITSBITPIX               = USHORT_IMG;
            m_Statistics.dataType      = TUSHORT;
            m_Statistics.bytesPerPixel = sizeof(int16_t);
            break;
        case USHORT_IMG:
            m_Statistics.dataType      = TUSHORT;
            m_Statistics.bytesPerPixel = sizeof(uint16_t);
            break;
        case LONG_IMG:
            // Read LONG image as ULONG
            m_FITSBITPIX               = ULONG_IMG;
            m_Statistics.dataType      = TULONG;
            m_Statistics.bytesPerPixel = sizeof(int32_t);
            break;
        case ULONG_IMG:
            m_Statistics.dataType      = TULONG;
            m_Statistics.bytesPerPixel = sizeof(uint32_t);
            break;
        case FLOAT_IMG:
            m_Statistics.dataType      = TFLOAT;
            m_Statistics.bytesPerPixel = sizeof(float);
            break;
        case LONGLONG_IMG:
            m_Statistics.dataType      = TLONGLONG;
            m_Statistics.bytesPerPixel = sizeof(int64_t);
            break;
        case DOUBLE_IMG:
            m_Statistics.dataType      = TDOUBLE;
            m_Statistics.bytesPerPixel = sizeof(double);
            break;
        default:
            return false;
    }

    if (m_Statistics.ndim < 3)
        naxes[2] = 1;

    if (naxes[0] == 0 || naxes[1] == 0)
    {
        free(m_PackBuffer);
        m_PackBuffer = nullptr;
        return false;
    }

    m_Statistics.width               = naxes[0];
    m_Statistics.height              = naxes[1];
    m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
    roiCenter.setX(m_Statistics.width / 2);
    roiCenter.setY(m_Statistics.height / 2);
    if(m_Statistics.width % 2)
        roiCenter.setX(roiCenter.x() + 1);
    if(m_Statistics.height % 2)
        roiCenter.setY(roiCenter.y() + 1);

    clearImageBuffers();

    m_Statistics.channels = naxes[2];

    m_ImageBufferSize = m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel;
    m_ImageBuffer = new uint8_t[m_ImageBufferSize];
    if (m_ImageBuffer == nullptr)
    {
        clearImageBuffers();
        free(m_PackBuffer);
        m_PackBuffer = nullptr;
        return false;
    }

    rotCounter     = 0;
    flipHCounter   = 0;
    flipVCounter   = 0;
    long nelements = m_Statistics.samples_per_channel * m_Statistics.channels;

    if (fits_read_img(fptr, m_Statistics.dataType, 1, nelements, nullptr, m_ImageBuffer, &anynull, &status))
    {
        return false;
    }

    parseHeader();

    // Get UTC date time
    QVariant value;
    if (getRecordValue("DATE-OBS", value) && value.isValid())
    {
        m_DateTime = value.toDateTime();
    }

    // Only check for debayed IF the original naxes[2] is 1
    // which is for single channels.
    if (naxes[2] == 1 && m_Statistics.channels == 1 && checkDebayer())
    {
        // Save bayer image on disk in case we need to save it later since debayer destorys this data
        if (m_isTemporary && m_TemporaryDataFile.open())
        {
            m_TemporaryDataFile.write(buffer);
            m_TemporaryDataFile.close();
            m_Filename = m_TemporaryDataFile.fileName();
        }

        if (debayer())
            calculateStats(false, false);
    }
    else
        calculateStats(false, false);

    if (m_Mode == FITS_NORMAL || m_Mode == FITS_ALIGN)
        loadWCS();

    starsSearched = false;

    return true;
}

bool FITSData::loadXISFImage(const QByteArray &buffer)
{
    m_HistogramConstructed = false;
    clearImageBuffers();

    try
    {
        LibXISF::XISFReader xisfReader;
        if (buffer.isEmpty())
        {
            xisfReader.open(m_Filename.toLocal8Bit().data());
        }
        else
        {
            LibXISF::ByteArray byteArray(buffer.constData(), buffer.size());
            xisfReader.open(byteArray);
        }

        if (xisfReader.imagesCount() == 0)
        {
            return false;
        }

        const LibXISF::Image &image = xisfReader.getImage(0);

        switch (image.sampleFormat())
        {
            case LibXISF::Image::UInt8:
                m_Statistics.dataType = TBYTE;
                m_Statistics.bytesPerPixel = sizeof(LibXISF::UInt8);
                m_FITSBITPIX = TBYTE;
                break;
            case LibXISF::Image::UInt16:
                m_Statistics.dataType = TUSHORT;
                m_Statistics.bytesPerPixel = sizeof(LibXISF::UInt16);
                m_FITSBITPIX = TUSHORT;
                break;
            case LibXISF::Image::UInt32:
                m_Statistics.dataType = TULONG;
                m_Statistics.bytesPerPixel = sizeof(LibXISF::UInt32);
                m_FITSBITPIX = TULONG;
                break;
            case LibXISF::Image::Float32:
                m_Statistics.dataType = TFLOAT;
                m_Statistics.bytesPerPixel = sizeof(LibXISF::Float32);
                m_FITSBITPIX = TFLOAT;
                break;
            default:
                return false;
        }

        m_Statistics.width = image.width();
        m_Statistics.height = image.height();
        m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
        m_Statistics.channels = image.channelCount();
        m_Statistics.size = buffer.size();
        roiCenter.setX(m_Statistics.width / 2);
        roiCenter.setY(m_Statistics.height / 2);
        if(m_Statistics.width % 2)
            roiCenter.setX(roiCenter.x() + 1);
        if(m_Statistics.height % 2)
            roiCenter.setY(roiCenter.y() + 1);

        m_HeaderRecords.clear();
        auto &fitsKeywords = image.fitsKeywords();
        for(auto &fitsKeyword : fitsKeywords)
            m_HeaderRecords.push_back({QString::fromStdString(fitsKeyword.name), QString::fromStdString(fitsKeyword.value), QString::fromStdString(fitsKeyword.comment)});

        QVariant value;
        if (getRecordValue("DATE-OBS", value) && value.isValid())
        {
            m_DateTime = value.toDateTime();
        }

        m_ImageBufferSize = image.imageDataSize();
        m_ImageBuffer = new uint8_t[m_ImageBufferSize];
        std::memcpy(m_ImageBuffer, image.imageData(), m_ImageBufferSize);

        calculateStats(false, false);
        loadWCS();
    }
    catch (LibXISF::Error &error)
    {
        return false;
    }
    return true;
}

bool FITSData::saveXISFImage(const QString &newFilename)
{
    try
    {
        LibXISF::XISFWriter xisfWriter;
        LibXISF::Image image;
        image.setGeometry(m_Statistics.width, m_Statistics.height, m_Statistics.channels);
        if (m_Statistics.channels > 1)
            image.setColorSpace(LibXISF::Image::RGB);

        switch (m_FITSBITPIX)
        {
            case BYTE_IMG:
                image.setSampleFormat(LibXISF::Image::UInt8);
                break;
            case USHORT_IMG:
                image.setSampleFormat(LibXISF::Image::UInt16);
                break;
            case ULONG_IMG:
                image.setSampleFormat(LibXISF::Image::UInt32);
                break;
            case FLOAT_IMG:
                image.setSampleFormat(LibXISF::Image::Float32);
                break;
            default:
                return false;
        }

        std::memcpy(image.imageData(), m_ImageBuffer, m_ImageBufferSize);
        for (auto &fitsKeyword : m_HeaderRecords)
            image.addFITSKeyword({fitsKeyword.key.toUtf8().data(), fitsKeyword.value.toString().toUtf8().data(), fitsKeyword.comment.toUtf8().data()});

        xisfWriter.writeImage(image);
        xisfWriter.save(newFilename.toLocal8Bit().data());
        m_Filename = newFilename;
    }
    catch (LibXISF::Error &err)
    {
        return false;
    }
    return true;
}

bool FITSData::loadCanonicalImage(const QByteArray &buffer)
{
    QImage imageFromFile;
    if (!buffer.isEmpty())
    {
        if(!imageFromFile.loadFromData(reinterpret_cast<const uint8_t * >(buffer.data()), buffer.size()))
        {
            return false;
        }

        m_Statistics.size = buffer.size();
    }
    else
    {
        if(!imageFromFile.load(m_Filename.toLocal8Bit()))
        {
            return false;
        }

        m_Statistics.size = QFileInfo(m_Filename).size();
    }

    //imageFromFile = imageFromFile.convertToFormat(QImage::Format_RGB32);

    // Note: This will need to be changed.  I think QT only loads 8 bpp images.
    // Also the depth method gives the total bits per pixel in the image not just the bits per
    // pixel in each channel.
    m_FITSBITPIX = BYTE_IMG;
    switch (m_FITSBITPIX)
    {
        case BYTE_IMG:
            m_Statistics.dataType      = TBYTE;
            m_Statistics.bytesPerPixel = sizeof(uint8_t);
            break;
        case SHORT_IMG:
            // Read SHORT image as USHORT
            m_Statistics.dataType      = TUSHORT;
            m_Statistics.bytesPerPixel = sizeof(int16_t);
            break;
        case USHORT_IMG:
            m_Statistics.dataType      = TUSHORT;
            m_Statistics.bytesPerPixel = sizeof(uint16_t);
            break;
        case LONG_IMG:
            // Read LONG image as ULONG
            m_Statistics.dataType      = TULONG;
            m_Statistics.bytesPerPixel = sizeof(int32_t);
            break;
        case ULONG_IMG:
            m_Statistics.dataType      = TULONG;
            m_Statistics.bytesPerPixel = sizeof(uint32_t);
            break;
        case FLOAT_IMG:
            m_Statistics.dataType      = TFLOAT;
            m_Statistics.bytesPerPixel = sizeof(float);
            break;
        case LONGLONG_IMG:
            m_Statistics.dataType      = TLONGLONG;
            m_Statistics.bytesPerPixel = sizeof(int64_t);
            break;
        case DOUBLE_IMG:
            m_Statistics.dataType      = TDOUBLE;
            m_Statistics.bytesPerPixel = sizeof(double);
            break;
        default:
            m_LastError = QString("Bit depth %1 is not supported.").arg(m_FITSBITPIX);
            return false;
    }

    m_Statistics.width = static_cast<uint16_t>(imageFromFile.width());
    m_Statistics.height = static_cast<uint16_t>(imageFromFile.height());
    m_Statistics.channels = imageFromFile.format() == QImage::Format_Grayscale8 ? 1 : 3;
    m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
    clearImageBuffers();
    m_ImageBufferSize = m_Statistics.samples_per_channel * m_Statistics.channels * static_cast<uint16_t>
                        (m_Statistics.bytesPerPixel);
    m_ImageBuffer = new uint8_t[m_ImageBufferSize];
    if (m_ImageBuffer == nullptr)
    {
        clearImageBuffers();
        return false;
    }

    if (m_Statistics.channels == 1)
    {
        memcpy(m_ImageBuffer, imageFromFile.bits(), m_ImageBufferSize);
    }
    else
    {

        auto debayered_buffer = reinterpret_cast<uint8_t *>(m_ImageBuffer);
        auto * original_bayered_buffer = reinterpret_cast<uint8_t *>(imageFromFile.bits());

        // Data in RGBA, with bytes in the order of B,G,R we need to copy them into 3 layers for FITS
        uint8_t * rBuff = debayered_buffer;
        uint8_t * gBuff = debayered_buffer + (m_Statistics.width * m_Statistics.height);
        uint8_t * bBuff = debayered_buffer + (m_Statistics.width * m_Statistics.height * 2);

        int imax = m_Statistics.samples_per_channel * 4 - 4;
        for (int i = 0; i <= imax; i += 4)
        {
            *rBuff++ = original_bayered_buffer[i + 2];
            *gBuff++ = original_bayered_buffer[i + 1];
            *bBuff++ = original_bayered_buffer[i + 0];
        }
    }

    calculateStats(false, false);
    return true;
}

bool FITSData::loadFromOpenCV(const cv::Mat &image, const QString &inFilename)
{
    loadCommon(inFilename);
    m_Extension = QFileInfo(inFilename).suffix().toLower();

    // Convert to grayscale if needed
    cv::Mat gray;
    if (image.channels() > 1)
    {
        cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
    }
    else
    {
        gray = image.clone();
    }

    // Convert to 8-bit if needed
    if (gray.type() != CV_8UC1)
    {
        gray.convertTo(gray, CV_8UC1);
    }

    // Set up statistics
    m_FITSBITPIX = BYTE_IMG;
    m_Statistics.dataType = TBYTE;
    m_Statistics.bytesPerPixel = sizeof(uint8_t);
    m_Statistics.width = static_cast<uint16_t>(gray.cols);
    m_Statistics.height = static_cast<uint16_t>(gray.rows);
    m_Statistics.channels = 1;
    m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
    m_Statistics.size = m_Statistics.samples_per_channel * m_Statistics.bytesPerPixel;

    // Allocate memory for image buffer
    clearImageBuffers();
    m_ImageBufferSize = m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel;
    m_ImageBuffer = new uint8_t[m_ImageBufferSize];
    if (m_ImageBuffer == nullptr)
    {
        clearImageBuffers();
        return false;
    }

    // Copy data from OpenCV matrix to buffer
    memcpy(m_ImageBuffer, gray.data, m_ImageBufferSize);

    // Calculate statistics
    calculateStats(false, false);

    return true;
}

bool FITSData::loadRAWImage(const QByteArray &buffer)
{
    int ret = 0;
    // Creation of image processing object
    LibRaw RawProcessor;

    // Let us open the file/buffer
    if (buffer.isEmpty())
    {
        // Open file
        if ((ret = RawProcessor.open_file(m_Filename.toLocal8Bit().constData())) != LIBRAW_SUCCESS)
        {
            RawProcessor.recycle();
            return false;
        }

        m_Statistics.size = QFileInfo(m_Filename).size();
    }
    // Open Buffer
    else
    {
        if ((ret = RawProcessor.open_buffer(const_cast<void *>(reinterpret_cast<const void *>(buffer.data())),
                                            buffer.size())) != LIBRAW_SUCCESS)
        {
            RawProcessor.recycle();
            return false;
        }

        m_Statistics.size = buffer.size();
    }

    // Let us unpack the thumbnail
    if ((ret = RawProcessor.unpack()) != LIBRAW_SUCCESS)
    {
        RawProcessor.recycle();
        return false;
    }

    if ((ret = RawProcessor.dcraw_process()) != LIBRAW_SUCCESS)
    {
        RawProcessor.recycle();
        return false;
    }

    libraw_processed_image_t *image = RawProcessor.dcraw_make_mem_image(&ret);
    if (ret != LIBRAW_SUCCESS)
    {
        RawProcessor.recycle();
        return false;
    }

    RawProcessor.recycle();

    m_Statistics.bytesPerPixel = image->bits / 8;
    // We only support two types now
    if (m_Statistics.bytesPerPixel == 1)
        m_Statistics.dataType = TBYTE;
    else
        m_Statistics.dataType = TUSHORT;
    m_Statistics.width = image->width;
    m_Statistics.height = image->height;
    m_Statistics.channels = image->colors;
    m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
    clearImageBuffers();
    m_ImageBufferSize = m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel;
    m_ImageBuffer = new uint8_t[m_ImageBufferSize];
    if (m_ImageBuffer == nullptr)
    {
        libraw_dcraw_clear_mem(image);
        clearImageBuffers();
        return false;
    }

    auto destination_buffer = reinterpret_cast<uint8_t *>(m_ImageBuffer);
    auto source_buffer = reinterpret_cast<uint8_t *>(image->data);

    // For mono, we memcpy directly
    if (image->colors == 1)
    {
        memcpy(destination_buffer, source_buffer, m_ImageBufferSize);
    }
    else
    {
        // Data in RGB24, with bytes in the order of R,G,B. We copy them copy them into 3 layers for FITS
        uint8_t * rBuff = destination_buffer;
        uint8_t * gBuff = destination_buffer + (m_Statistics.width * m_Statistics.height);
        uint8_t * bBuff = destination_buffer + (m_Statistics.width * m_Statistics.height * 2);

        int imax = m_Statistics.samples_per_channel * 3 - 3;
        for (int i = 0; i <= imax; i += 3)
        {
            *rBuff++ = source_buffer[i + 0];
            *gBuff++ = source_buffer[i + 1];
            *bBuff++ = source_buffer[i + 2];
        }
    }
    libraw_dcraw_clear_mem(image);

    calculateStats(false, false);
    return true;
}

bool FITSData::saveImage(const QString &newFilename)
{
    if (newFilename == m_Filename)
        return true;

    const QString ext = QFileInfo(newFilename).suffix();

    if (ext == "jpg" || ext == "png")
    {
        double min, max;
        QImage fitsImage = FITSToImage(newFilename, -1, -1);
        getMinMax(&min, &max);

        if (min == max)
        {
            fitsImage.fill(Qt::white);
        }
        else if (channels() == 1)
        {
            fitsImage = QImage(width(), height(), QImage::Format_Indexed8);

            fitsImage.setColorCount(256);
            for (int i = 0; i < 256; i++)
                fitsImage.setColor(i, qRgb(i, i, i));
        }
        else
        {
            fitsImage = QImage(width(), height(), QImage::Format_RGB32);
        }

        double dataMin = m_Statistics.mean[0] - m_Statistics.stddev[0];
        double dataMax = m_Statistics.mean[0] + m_Statistics.stddev[0] * 3;

        double bscale = 255. / (dataMax - dataMin);
        double bzero  = (-dataMin) * (255. / (dataMax - dataMin));

        // Long way to do this since we do not want to use templated functions here
        switch (m_Statistics.dataType)
        {
            case TBYTE:
                convertToQImage<uint8_t>(dataMin, dataMax, bscale, bzero, fitsImage);
                break;

            case TSHORT:
                convertToQImage<int16_t>(dataMin, dataMax, bscale, bzero, fitsImage);
                break;

            case TUSHORT:
                convertToQImage<uint16_t>(dataMin, dataMax, bscale, bzero, fitsImage);
                break;

            case TLONG:
                convertToQImage<int32_t>(dataMin, dataMax, bscale, bzero, fitsImage);
                break;

            case TULONG:
                convertToQImage<uint32_t>(dataMin, dataMax, bscale, bzero, fitsImage);
                break;

            case TFLOAT:
                convertToQImage<float>(dataMin, dataMax, bscale, bzero, fitsImage);
                break;

            case TLONGLONG:
                convertToQImage<int64_t>(dataMin, dataMax, bscale, bzero, fitsImage);
                break;

            case TDOUBLE:
                convertToQImage<double>(dataMin, dataMax, bscale, bzero, fitsImage);
                break;

            default:
                break;
        }

        fitsImage.save(newFilename, ext.toLatin1().constData());

        m_Filename = newFilename;
        return true;
    }

    if (m_isCompressed)
    {
        return false;
    }

    int status = 0;
    long nelements;
    fitsfile * new_fptr;

    if (ext == "xisf")
    {
        if(fptr)
        {
            fits_close_file(fptr, &status);
            fptr = nullptr;
        }
        rotCounter = flipHCounter = flipVCounter = 0;
        return saveXISFImage(newFilename);
    }

    //    if (HasDebayer && m_Filename.isEmpty() == false)
    //    {
    //        fits_flush_file(fptr, &status);
    //        /* close current file */
    //        if (fits_close_file(fptr, &status))
    //        {
    //            recordLastError(status);
    //            return status;
    //        }

    //        // Skip "!" in the beginning of the new file name
    //        QString finalFileName(newFilename);

    //        finalFileName.remove('!');

    //        // Remove first otherwise copy will fail below if file exists
    //        QFile::remove(finalFileName);

    //        if (!QFile::copy(m_Filename, finalFileName))
    //        {
    //            qCCritical(KSTARS_FITS()) << "FITS: Failed to copy " << m_Filename << " to " << finalFileName;
    //            fptr = nullptr;
    //            return false;
    //        }

    //        m_Filename = finalFileName;

    //        // Use open diskfile as it does not use extended file names which has problems opening
    //        // files with [ ] or ( ) in their names.
    //        fits_open_diskfile(&fptr, m_Filename.toLocal8Bit(), READONLY, &status);
    //        fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status);

    //        return true;
    //    }

    // Read the image back into buffer in case we debyayed
    nelements = m_Statistics.samples_per_channel * m_Statistics.channels;

    /* close current file */
    if (fptr && fits_close_file(fptr, &status))
    {
        return false;
    }

    /* Create a new File, overwriting existing*/
    if (fits_create_file(&new_fptr, QString("!%1").arg(newFilename).toLocal8Bit(), &status))
    {
        return status;
    }

    status = 0;

    fptr = new_fptr;

    // Create image
    long naxis = m_Statistics.channels == 1 ? 2 : 3;
    long naxes[3] = {m_Statistics.width, m_Statistics.height, naxis};

    // JM 2020-12-28: Here we to use bitpix values
    if (fits_create_img(fptr, m_FITSBITPIX, naxis, naxes, &status))
    {
        return false;
    }

    /* Write keywords */

    // Minimum
    if (fits_update_key(fptr, TDOUBLE, "DATAMIN", &(m_Statistics.min), "Minimum value", &status))
    {
        return false;
    }

    // Maximum
    if (fits_update_key(fptr, TDOUBLE, "DATAMAX", &(m_Statistics.max), "Maximum value", &status))
    {
        return false;
    }

    // KStars Min, for 3 channels
    fits_write_key(fptr, TDOUBLE, "MIN1", &m_Statistics.min[0], "Min Channel 1", &status);
    if (channels() > 1)
    {
        fits_write_key(fptr, TDOUBLE, "MIN2", &m_Statistics.min[1], "Min Channel 2", &status);
        fits_write_key(fptr, TDOUBLE, "MIN3", &m_Statistics.min[2], "Min Channel 3", &status);
    }

    // KStars max, for 3 channels
    fits_write_key(fptr, TDOUBLE, "MAX1", &m_Statistics.max[0], "Max Channel 1", &status);
    if (channels() > 1)
    {
        fits_write_key(fptr, TDOUBLE, "MAX2", &m_Statistics.max[1], "Max Channel 2", &status);
        fits_write_key(fptr, TDOUBLE, "MAX3", &m_Statistics.max[2], "Max Channel 3", &status);
    }

    // Mean
    if (m_Statistics.mean[0] > 0)
    {
        fits_write_key(fptr, TDOUBLE, "MEAN1", &m_Statistics.mean[0], "Mean Channel 1", &status);
        if (channels() > 1)
        {
            fits_write_key(fptr, TDOUBLE, "MEAN2", &m_Statistics.mean[1], "Mean Channel 2", &status);
            fits_write_key(fptr, TDOUBLE, "MEAN3", &m_Statistics.mean[2], "Mean Channel 3", &status);
        }
    }

    // Median
    if (m_Statistics.median[0] > 0)
    {
        fits_write_key(fptr, TDOUBLE, "MEDIAN1", &m_Statistics.median[0], "Median Channel 1", &status);
        if (channels() > 1)
        {
            fits_write_key(fptr, TDOUBLE, "MEDIAN2", &m_Statistics.median[1], "Median Channel 2", &status);
            fits_write_key(fptr, TDOUBLE, "MEDIAN3", &m_Statistics.median[2], "Median Channel 3", &status);
        }
    }

    // Standard Deviation
    if (m_Statistics.stddev[0] > 0)
    {
        fits_write_key(fptr, TDOUBLE, "STDDEV1", &m_Statistics.stddev[0], "Standard Deviation Channel 1", &status);
        if (channels() > 1)
        {
            fits_write_key(fptr, TDOUBLE, "STDDEV2", &m_Statistics.stddev[1], "Standard Deviation Channel 2", &status);
            fits_write_key(fptr, TDOUBLE, "STDDEV3", &m_Statistics.stddev[2], "Standard Deviation Channel 3", &status);
        }
    }

    // Skip first 10 standard records and copy the rest.
    for (int i = 10; i < m_HeaderRecords.count(); i++)
    {
        QString key = m_HeaderRecords[i].key;
        const char *comment = m_HeaderRecords[i].comment.toLatin1().constBegin();
        QVariant value = m_HeaderRecords[i].value;

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        switch (value.type())
        {
            case QVariant::Int:
            {
#else
    switch (value.typeId())
    {
        case QMetaType::Int:
        {
#endif
            int number = value.toInt();
            fits_write_key(fptr, TINT, key.toLatin1().constData(), &number, comment, &status);
        }
        break;

        case QVariant::Double:
        {
            double number = value.toDouble();
            fits_write_key(fptr, TDOUBLE, key.toLatin1().constData(), &number, comment, &status);
        }
        break;

        case QVariant::String:
        default:
        {
            char valueBuffer[256] = {0};
            strncpy(valueBuffer, value.toString().toLatin1().constData(), 256 - 1);
            fits_write_key(fptr, TSTRING, key.toLatin1().constData(), valueBuffer, comment, &status);
        }
    }
}

    // ISO Date
if (fits_write_date(fptr, &status))
{
    return false;
}

QString history =
    QString("Modified by KStars on %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"));
    // History
if (fits_write_history(fptr, history.toLatin1(), &status))
{
    return false;
}

int rot = 0, mirror = 0;
if (rotCounter > 0)
{
    rot = (90 * rotCounter) % 360;
    if (rot < 0)
        rot += 360;
}
if (flipHCounter % 2 != 0 || flipVCounter % 2 != 0)
    mirror = 1;

rotCounter = flipHCounter = flipVCounter = 0;

    // Here we need to use the actual data type
if (fits_write_img(fptr, m_Statistics.dataType, 1, nelements, m_ImageBuffer, &status))
{
    return false;
}

m_Filename = newFilename;

fits_flush_file(fptr, &status);

return true;
}

void FITSData::clearImageBuffers()
{
    delete[] m_ImageBuffer;
    m_ImageBuffer = nullptr;
    if(m_ImageRoiBuffer != nullptr )
    {
        delete[] m_ImageRoiBuffer;
        m_ImageRoiBuffer = nullptr;

    }
    //m_BayerBuffer = nullptr;
}

void FITSData::makeRoiBuffer(QRect roi)
{
    if (!roi.isValid())
        return;

    uint32_t channelSize = roi.height() * roi.width();
    if(channelSize  > m_Statistics.samples_per_channel || channelSize == 1)
    {
        return;
    }
    if(m_ImageRoiBuffer != nullptr )
    {
        delete[] m_ImageRoiBuffer;
        m_ImageRoiBuffer = nullptr;
    }
    int xoffset = roi.topLeft().x() - 1;
    int yoffset = roi.topLeft().y() - 1;
    uint32_t bpp = m_Statistics.bytesPerPixel;
    m_ImageRoiBuffer = new uint8_t[roi.height()*roi.width()*m_Statistics.channels * m_Statistics.bytesPerPixel]();
    for(int n = 0 ; n < m_Statistics.channels ; n++)
    {
        for(int i = 0; i < roi.height(); i++)
        {
            size_t i1 = n * channelSize * bpp +  i * roi.width() * bpp;
            size_t i2 = n * m_Statistics.samples_per_channel * bpp + (yoffset + i) * width() * bpp + xoffset * bpp;
            memcpy(&m_ImageRoiBuffer[i1],
                   &m_ImageBuffer[i2],
                   roi.width() * bpp);
        }

    }
    memcpy(&m_ROIStatistics, &m_Statistics, sizeof(FITSImage::Statistic));
    m_ROIStatistics.samples_per_channel = roi.height() * roi.width();
    m_ROIStatistics.width = roi.width();
    m_ROIStatistics.height = roi.height();
    calculateStats(false, true);
}
void FITSData::calculateStats(bool refresh, bool roi)
{
    // Calculate min max
    if(roi == false)
    {
        calculateMinMax(refresh);
        calculateMedian(refresh);

        // Try to read mean/median/stddev if in file
        if (refresh == false && fptr)
        {
            int status = 0;
            int nfound = 0;
            if (fits_read_key_dbl(fptr, "MEAN1", &m_Statistics.mean[0], nullptr, &status) == 0)
                nfound++;
            // NB. These could fail if missing, which is OK.
            fits_read_key_dbl(fptr, "MEAN2", & m_Statistics.mean[1], nullptr, &status);
            fits_read_key_dbl(fptr, "MEAN3", &m_Statistics.mean[2], nullptr, &status);

            status = 0;
            if (fits_read_key_dbl(fptr, "STDDEV1", &m_Statistics.stddev[0], nullptr, &status) == 0)
                nfound++;
            // NB. These could fail if missing, which is OK.
            fits_read_key_dbl(fptr, "STDDEV2", &m_Statistics.stddev[1], nullptr, &status);
            fits_read_key_dbl(fptr, "STDDEV3", &m_Statistics.stddev[2], nullptr, &status);

            // If all is OK, we're done
            if (nfound == 2)
                return;
        }

        // Get standard deviation and mean in one run
        switch (m_Statistics.dataType)
        {
            case TBYTE:
                calculateStdDev<uint8_t>();
                break;

            case TSHORT:
                calculateStdDev<int16_t>();
                break;

            case TUSHORT:
                calculateStdDev<uint16_t>();
                break;

            case TLONG:
                calculateStdDev<int32_t>();
                break;

            case TULONG:
                calculateStdDev<uint32_t>();
                break;

            case TFLOAT:
                calculateStdDev<float>();
                break;

            case TLONGLONG:
                calculateStdDev<int64_t>();
                break;

            case TDOUBLE:
                calculateStdDev<double>();
                break;

            default:
                return;
        }

        // FIXME That's not really SNR, must implement a proper solution for this value
        m_Statistics.SNR = m_Statistics.mean[0] / m_Statistics.stddev[0];
    }
    else
    {
        calculateMinMax(refresh, roi);
        calculateMedian(refresh, roi);

        switch (m_ROIStatistics.dataType)
        {
            case TBYTE:
                calculateStdDev<uint8_t>(roi);
                break;

            case TSHORT:
                calculateStdDev<int16_t>(roi);
                break;

            case TUSHORT:
                calculateStdDev<uint16_t>(roi);
                break;

            case TLONG:
                calculateStdDev<int32_t>(roi);
                break;

            case TULONG:
                calculateStdDev<uint32_t>(roi);
                break;

            case TFLOAT:
                calculateStdDev<float>(roi);
                break;

            case TLONGLONG:
                calculateStdDev<int64_t>(roi);
                break;

            case TDOUBLE:
                calculateStdDev<double>(roi);
                break;

            default:
                return;
        }
    }
}

void FITSData::calculateMinMax(bool refresh, bool roi)
{
    if(roi == false)
    {
        int status, nfound = 0;

        status = 0;

        // Only fetch from header if we have a single channel
        // Otherwise, calculate manually.
        if (fptr != nullptr && !refresh)
        {

            status = 0;

            if (fits_read_key_dbl(fptr, "DATAMIN", &(m_Statistics.min[0]), nullptr, &status) == 0)
                nfound++;
            else if (fits_read_key_dbl(fptr, "MIN1", &(m_Statistics.min[0]), nullptr, &status) == 0)
                nfound++;

            // NB. These could fail if missing, which is OK.
            fits_read_key_dbl(fptr, "MIN2", &m_Statistics.min[1], nullptr, &status);
            fits_read_key_dbl(fptr, "MIN3", &m_Statistics.min[2], nullptr, &status);

            status = 0;

            if (fits_read_key_dbl(fptr, "DATAMAX", &(m_Statistics.max[0]), nullptr, &status) == 0)
                nfound++;
            else if (fits_read_key_dbl(fptr, "MAX1", &(m_Statistics.max[0]), nullptr, &status) == 0)
                nfound++;

            // NB. These could fail if missing, which is OK.
            fits_read_key_dbl(fptr, "MAX2", &m_Statistics.max[1], nullptr, &status);
            fits_read_key_dbl(fptr, "MAX3", &m_Statistics.max[2], nullptr, &status);

            // If we found both keywords, no need to calculate them, unless they are both zeros
            if (nfound == 2 && !(m_Statistics.min[0] == 0 && m_Statistics.max[0] == 0))
                return;
        }

        m_Statistics.min[0] = 1.0E30;
        m_Statistics.max[0] = -1.0E30;

        m_Statistics.min[1] = 1.0E30;
        m_Statistics.max[1] = -1.0E30;

        m_Statistics.min[2] = 1.0E30;
        m_Statistics.max[2] = -1.0E30;

        switch (m_Statistics.dataType)
        {
            case TBYTE:
                calculateMinMax<uint8_t>();
                break;

            case TSHORT:
                calculateMinMax<int16_t>();
                break;

            case TUSHORT:
                calculateMinMax<uint16_t>();
                break;

            case TLONG:
                calculateMinMax<int32_t>();
                break;

            case TULONG:
                calculateMinMax<uint32_t>();
                break;

            case TFLOAT:
                calculateMinMax<float>();
                break;

            case TLONGLONG:
                calculateMinMax<int64_t>();
                break;

            case TDOUBLE:
                calculateMinMax<double>();
                break;

            default:
                break;
        }
    }
    else
    {
        m_ROIStatistics.min[0] = 1.0E30;
        m_ROIStatistics.max[0] = -1.0E30;

        m_ROIStatistics.min[1] = 1.0E30;
        m_ROIStatistics.max[1] = -1.0E30;

        m_ROIStatistics.min[2] = 1.0E30;
        m_ROIStatistics.max[2] = -1.0E30;

        switch (m_Statistics.dataType)
        {
            case TBYTE:
                calculateMinMax<uint8_t>(roi);
                break;

            case TSHORT:
                calculateMinMax<int16_t>(roi);
                break;

            case TUSHORT:
                calculateMinMax<uint16_t>(roi);
                break;

            case TLONG:
                calculateMinMax<int32_t>(roi);
                break;

            case TULONG:
                calculateMinMax<uint32_t>(roi);
                break;

            case TFLOAT:
                calculateMinMax<float>(roi);
                break;

            case TLONGLONG:
                calculateMinMax<int64_t>(roi);
                break;

            case TDOUBLE:
                calculateMinMax<double>(roi);
                break;

            default:
                break;
        }

    }
}

void FITSData::calculateMedian(bool refresh, bool roi)
{
    if(!roi)
    {
        int status, nfound = 0;

        status = 0;

        // Only fetch from header if we have a single channel
        // Otherwise, calculate manually.
        if (fptr != nullptr && !refresh)
        {
            status = 0;
            if (fits_read_key_dbl(fptr, "MEDIAN1", &m_Statistics.median[0], nullptr, &status) == 0)
                nfound++;

            // NB. These could fail if missing, which is OK.
            fits_read_key_dbl(fptr, "MEDIAN2", &m_Statistics.median[1], nullptr, &status);
            fits_read_key_dbl(fptr, "MEDIAN3", &m_Statistics.median[2], nullptr, &status);

            if (nfound == 1)
                return;
        }

        m_Statistics.median[RED_CHANNEL] = 0;
        m_Statistics.median[GREEN_CHANNEL] = 0;
        m_Statistics.median[BLUE_CHANNEL] = 0;

        switch (m_Statistics.dataType)
        {
            case TBYTE:
                calculateMedian<uint8_t>();
                break;

            case TSHORT:
                calculateMedian<int16_t>();
                break;

            case TUSHORT:
                calculateMedian<uint16_t>();
                break;

            case TLONG:
                calculateMedian<int32_t>();
                break;

            case TULONG:
                calculateMedian<uint32_t>();
                break;

            case TFLOAT:
                calculateMedian<float>();
                break;

            case TLONGLONG:
                calculateMedian<int64_t>();
                break;

            case TDOUBLE:
                calculateMedian<double>();
                break;

            default:
                break;
        }
    }
    else
    {
        m_ROIStatistics.median[RED_CHANNEL] = 0;
        m_ROIStatistics.median[GREEN_CHANNEL] = 0;
        m_ROIStatistics.median[BLUE_CHANNEL] = 0;

        switch (m_ROIStatistics.dataType)
        {
            case TBYTE:
                calculateMedian<uint8_t>(roi);
                break;

            case TSHORT:
                calculateMedian<int16_t>(roi);
                break;

            case TUSHORT:
                calculateMedian<uint16_t>(roi);
                break;

            case TLONG:
                calculateMedian<int32_t>(roi);
                break;

            case TULONG:
                calculateMedian<uint32_t>(roi);
                break;

            case TFLOAT:
                calculateMedian<float>(roi);
                break;

            case TLONGLONG:
                calculateMedian<int64_t>(roi);
                break;

            case TDOUBLE:
                calculateMedian<double>(roi);
                break;

            default:
                break;
        }

    }
}

template <typename T>
void FITSData::calculateMedian(bool roi)
{
    auto * buffer = reinterpret_cast<T *>(roi ? m_ImageRoiBuffer : m_ImageBuffer);
    const uint32_t maxMedianSize = 500000;
    uint32_t medianSize = roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel;
    uint8_t downsample = 1;
    if (medianSize > maxMedianSize)
    {
        downsample = (static_cast<double>(medianSize) / maxMedianSize) + 0.999;
        medianSize /= downsample;
    }
    // Ideally samples would be declared like this...
    //std::vector<T> samples;
    // Unfortunately this doesn't compile on Mac - see the comments in robuststatistics.cpp for more details
    // So for now declare samples like this...
    std::vector<int32_t> samples;
    samples.reserve(medianSize);

    for (uint8_t n = 0; n < m_Statistics.channels; n++)
    {
        auto *oneChannel = buffer + n * (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);
        for (uint32_t upto = 0; upto < (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);
                upto += downsample)
            samples.push_back(oneChannel[upto]);

        auto median = Mathematics::RobustStatistics::ComputeLocation(Mathematics::RobustStatistics::LOCATION_MEDIAN, samples);
        roi ? m_ROIStatistics.median[n] = median : m_Statistics.median[n] = median;
    }
}

template <typename T>
QPair<T, T> FITSData::getParitionMinMax(uint32_t start, uint32_t stride, bool roi)
{
    auto * buffer = reinterpret_cast<T *>(roi ? m_ImageRoiBuffer : m_ImageBuffer);
    T min = std::numeric_limits<T>::max();
    T max = std::numeric_limits<T>::min();

    uint32_t end = start + stride;

    for (uint32_t i = start; i < end; i++)
    {
        min = qMin(buffer[i], min);
        max = qMax(buffer[i], max);
        //        if (buffer[i] < min)
        //            min = buffer[i];
        //        else if (buffer[i] > max)
        //            max = buffer[i];
    }

    return qMakePair(min, max);
}

template <typename T>
void FITSData::calculateMinMax(bool roi)
{
    T min = std::numeric_limits<T>::max();
    T max = std::numeric_limits<T>::min();


    // Create N threads
    const uint8_t nThreads = 16;

    for (int n = 0; n < m_Statistics.channels; n++)
    {
        uint32_t cStart = n * (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);

        // Calculate how many elements we process per thread
        uint32_t tStride = (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) / nThreads;

        // Calculate the final stride since we can have some left over due to division above
        uint32_t fStride = tStride + ((roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) -
                                      (tStride * nThreads));

        // Start location for inspecting elements
        uint32_t tStart = cStart;

        // List of futures
        QList<QFuture<QPair<T, T >>> futures;

        for (int i = 0; i < nThreads; i++)
        {
            // Run threads
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
            futures.append(QtConcurrent::run(this, &FITSData::getParitionMinMax<T>, tStart, (i == (nThreads - 1)) ? fStride : tStride,
                                             roi));
#else
            futures.append(QtConcurrent::run(&FITSData::getParitionMinMax<T>, this, tStart, (i == (nThreads - 1)) ? fStride : tStride,
                                             roi));
#endif
            tStart += tStride;
        }

        // Now wait for results
        for (int i = 0; i < nThreads; i++)
        {
            QPair<T, T> result = futures[i].result();
            min = qMin(result.first, min);
            max = qMax(result.second, max);
        }

        if(!roi)
        {
            m_Statistics.min[n] = min;
            m_Statistics.max[n] = max;
        }
        else
        {
            m_ROIStatistics.min[n] = min;
            m_ROIStatistics.max[n] = max;
        }
    }

}

// This struct is used when returning results from the threaded getSumAndSquaredSum calculations
// used to compute the mean and variance of the image.
struct SumData
{
    double sum;
    double squaredSum;
    double numSamples;
    SumData(double s, double sq, int n) : sum(s), squaredSum(sq), numSamples(n) {}
    SumData() : sum(0), squaredSum(0), numSamples(0) {}
};

template <typename T>
SumData getSumAndSquaredSum(uint32_t start, uint32_t stride, uint8_t *buff)
{
    auto * buffer = reinterpret_cast<T *>(buff);
    const uint32_t end = start + stride;
    double sum = 0;
    double squaredSum = 0;
    for (uint32_t i = start; i < end; i++)
    {
        double sample = buffer[i];
        sum += sample;
        squaredSum += sample * sample;
    }
    const double numSamples = end - start;
    return SumData(sum, squaredSum, numSamples);
}

template <typename T>
void FITSData::calculateStdDev(bool roi )
{
    // Create N threads
    const uint8_t nThreads = 16;

    for (int n = 0; n < m_Statistics.channels; n++)
    {
        uint32_t cStart = n * (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);

        // Calculate how many elements we process per thread
        uint32_t tStride = (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) / nThreads;

        // Calculate the final stride since we can have some left over due to division above
        uint32_t fStride = tStride + ((roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) -
                                      (tStride * nThreads));

        // Start location for inspecting elements
        uint32_t tStart = cStart;

        // List of futures
        QList<QFuture<SumData >> futures;

        for (int i = 0; i < nThreads; i++)
        {
            // Run threads
            uint8_t *buff = roi ? m_ImageRoiBuffer : m_ImageBuffer;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
            futures.append(QtConcurrent::run(&getSumAndSquaredSum<T>, tStart,
                                             (i == (nThreads - 1)) ? fStride : tStride, buff));
#else
            futures.append(QtConcurrent::run(getSumAndSquaredSum<T>, tStart,
                                             (i == (nThreads - 1)) ? fStride : tStride, buff));
#endif
            tStart += tStride;
        }

        // Now wait for results
        double sum = 0, squared_sum = 0;
        double numSamples = 0;
        for (int i = 0; i < nThreads; i++)
        {
            const SumData result = futures[i].result();
            sum += result.sum;
            squared_sum += result.squaredSum;
            numSamples += result.numSamples;

        }
        if (numSamples <= 0) continue;
        const double mean = sum / numSamples;
        const double variance = squared_sum / numSamples - mean * mean;
        if(!roi)
        {
            m_Statistics.mean[n]   = mean;
            m_Statistics.stddev[n] = sqrt(variance);
        }
        else
        {
            m_ROIStatistics.mean[n] = mean;
            m_ROIStatistics.stddev[n] = sqrt(variance);
        }
    }
}

QVector<double> FITSData::createGaussianKernel(int size, double sigma)
{
    QVector<double> kernel(size * size);
    kernel.fill(0.0, size * size);

    double kernelSum = 0.0;
    int fOff = (size - 1) / 2;
    double normal = 1.0 / (2.0 * M_PI * sigma * sigma);
    for (int y = -fOff; y <= fOff; y++)
    {
        for (int x = -fOff; x <= fOff; x++)
        {
            double distance = ((y * y) + (x * x)) / (2.0 * sigma * sigma);
            int index = (y + fOff) * size + (x + fOff);
            kernel[index] = normal * qExp(-distance);
            kernelSum += kernel.at(index);
        }
    }
    for (int y = 0; y < size; y++)
    {
        for (int x = 0; x < size; x++)
        {
            int index = y * size + x;
            kernel[index] = kernel.at(index) * 1.0 / kernelSum;
        }
    }

    return kernel;
}

template <typename T>
void FITSData::convolutionFilter(const QVector<double> &kernel, int kernelSize)
{
    T * imagePtr = reinterpret_cast<T *>(m_ImageBuffer);

    // Create variable for pixel data for each kernel
    T gt = 0;

    // This is how much your center pixel is offset from the border of your kernel
    int fOff = (kernelSize - 1) / 2;

    // Start with the pixel that is offset fOff from top and fOff from the left side
    // this is so entire kernel is on your image
    for (int offsetY = 0; offsetY < m_Statistics.height; offsetY++)
    {
        for (int offsetX = 0; offsetX < m_Statistics.width; offsetX++)
        {
            // reset gray value to 0
            gt = 0;
            // position of the kernel center pixel
            int byteOffset = offsetY * m_Statistics.width + offsetX;

            // kernel calculations
            for (int filterY = -fOff; filterY <= fOff; filterY++)
            {
                for (int filterX = -fOff; filterX <= fOff; filterX++)
                {
                    if ((offsetY + filterY) >= 0 && (offsetY + filterY) < m_Statistics.height
                            && ((offsetX + filterX) >= 0 && (offsetX + filterX) < m_Statistics.width ))
                    {

                        int calcOffset = byteOffset + filterX + filterY * m_Statistics.width;
                        int index = (filterY + fOff) * kernelSize + (filterX + fOff);
                        double kernelValue = kernel.at(index);
                        gt += (imagePtr[calcOffset]) * kernelValue;
                    }
                }
            }

            // set new data in the other byte array for your image data
            imagePtr[byteOffset] = gt;
        }
    }
}

template <typename T>
void FITSData::gaussianBlur(int kernelSize, double sigma)
{
    // Size must be an odd number!
    if (kernelSize % 2 == 0)
    {
        kernelSize--;
    }
    // Edge must be a positive number!
    if (kernelSize < 1)
    {
        kernelSize = 1;
    }

    QVector<double> gaussianKernel = createGaussianKernel(kernelSize, sigma);
    convolutionFilter<T>(gaussianKernel, kernelSize);
}

void FITSData::setMinMax(double newMin, double newMax, uint8_t channel)
{
    m_Statistics.min[channel] = newMin;
    m_Statistics.max[channel] = newMax;
}

bool FITSData::parseHeader()
{
    char * header = nullptr;
    int status = 0, nkeys = 0;

    if (fits_hdr2str(fptr, 0, nullptr, 0, &header, &nkeys, &status))
    {
        fits_report_error(stderr, status);
        free(header);
        return false;
    }

    m_HeaderRecords.clear();
    QString recordList = QString(header);

    for (int i = 0; i < nkeys; i++)
    {
        Record oneRecord;
        // Quotes cause issues for simplified below so we're removing them.
        QString record = recordList.mid(i * 80, 80).remove("'");
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        QStringList properties = record.split(QRegExp("[=/]"));
#else
        QStringList properties = record.split(QRegularExpression("[=/]"));
#endif
        // If it is only a comment
        if (properties.size() == 1)
        {
            oneRecord.key = properties[0].mid(0, 7);
            oneRecord.comment = properties[0].mid(8).simplified();
        }
        else
        {
            oneRecord.key = properties[0].simplified();
            oneRecord.value = properties[1].simplified();
            if (properties.size() > 2)
                oneRecord.comment = properties[2].simplified();

            // Try to guess the value.
            // Test for integer & double. If neither, then leave it as "string".
            bool ok = false;

            // Is it Integer?
            oneRecord.value.toInt(&ok);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
            if (ok)
                oneRecord.value.convert(QMetaType::Int);
            else
            {
                // Is it double?
                oneRecord.value.toDouble(&ok);
                if (ok)
                    oneRecord.value.convert(QMetaType::Double);
            }
#else
            if (ok)
                oneRecord.value = QVariant(oneRecord.value.toInt());
            else
            {
                // Is it double?
                oneRecord.value.toDouble(&ok);
                if (ok)
                    oneRecord.value = QVariant(oneRecord.value.toDouble());
            }
#endif
        }

        m_HeaderRecords.append(oneRecord);
    }

    free(header);

    return true;
}

bool FITSData::getRecordValue(const QString &key, QVariant &value) const
{
    auto result = std::find_if(m_HeaderRecords.begin(), m_HeaderRecords.end(), [&key](const Record & oneRecord)
    {
        return (oneRecord.key == key && oneRecord.value.isValid());
    });

    if (result != m_HeaderRecords.end())
    {
        value = (*result).value;
        return true;
    }
    return false;
}

bool FITSData::loadWCS()
{
#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)

    if (m_WCSState == Success)
    {
        return true;
    }

    if (m_WCSHandle != nullptr)
    {
        wcsvfree(&m_nwcs, &m_WCSHandle);
        m_nwcs = 0;
        m_WCSHandle = nullptr;
    }


    QByteArray header_str;
    int status = 0;
    int nkeyrec = 0, nreject = 0;
    if (fptr)
    {
        char *header = nullptr;
        if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status))
        {
            char errmsg[512];
            fits_get_errstatus(status, errmsg);
            m_LastError = errmsg;
            m_WCSState = Failure;
            return false;
        }
        header_str = QByteArray(header);
        fits_free_memory(header, &status);
    }
    else
    {
        nkeyrec = 1;
        for(auto &fitsKeyword : m_HeaderRecords)
        {
            QByteArray rec;
            rec.append(fitsKeyword.key.leftJustified(8, ' ').toLatin1());
            rec.append("= ");
            rec.append(fitsKeyword.value.toByteArray());
            rec.append(" / ");
            rec.append(fitsKeyword.comment.toLatin1());
            header_str.append(rec.leftJustified(80, ' ', true));
            nkeyrec++;
        }
        header_str.append(QByteArray("END").leftJustified(80));
    }

    if ((status = wcspih(header_str.data(), nkeyrec, WCSHDR_all, 0, &nreject, &m_nwcs, &m_WCSHandle)) != 0)
    {
        wcsvfree(&m_nwcs, &m_WCSHandle);
        m_WCSHandle = nullptr;
        m_nwcs = 0;
        m_LastError = QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]);
        m_WCSState = Failure;
        return false;
    }

    if (m_WCSHandle == nullptr)
    {
        m_WCSState = Failure;
        return false;
    }

    // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now.
    if (m_WCSHandle->crpix[0] == 0)
    {
        wcsvfree(&m_nwcs, &m_WCSHandle);
        m_WCSHandle = nullptr;
        m_nwcs = 0;
        m_WCSState = Failure;
        return false;
    }

    cdfix(m_WCSHandle);
    if ((status = wcsset(m_WCSHandle)) != 0)
    {
        wcsvfree(&m_nwcs, &m_WCSHandle);
        m_WCSHandle = nullptr;
        m_nwcs = 0;
        m_LastError = QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]);
        m_WCSState = Failure;
        return false;
    }

    m_ObjectsSearched = false;
    m_WCSState = Success;
    HasWCS = true;

    return true;
#else
    return false;
#endif
}

uint8_t * FITSData::getWritableImageBuffer()
{
    return m_ImageBuffer;
}

uint8_t const * FITSData::getImageBuffer() const
{
    return m_ImageBuffer;
}

void FITSData::setImageBuffer(uint8_t * buffer)
{
    delete[] m_ImageBuffer;
    m_ImageBuffer = buffer;
}

bool FITSData::checkDebayer()
{
    int status = 0;
    char bayerPattern[64], roworder[64];

    // Let's search for BAYERPAT keyword, if it's not found we return as there is no bayer pattern in this image
    if (fits_read_keyword(fptr, "BAYERPAT", bayerPattern, nullptr, &status))
        return false;

    if (m_Statistics.dataType != TUSHORT && m_Statistics.dataType != TBYTE)
    {
        return false;
    }
    QString pattern(bayerPattern);
    pattern = pattern.remove('\'').trimmed();

    QString order(roworder);
    order = order.remove('\'').trimmed();

    if (order == "BOTTOM-UP" && !(m_Statistics.height % 2))
    {
        if (pattern == "RGGB")
            pattern = "GBRG";
        else if (pattern == "GBRG")
            pattern = "RGGB";
        else if (pattern == "GRBG")
            pattern = "BGGR";
        else if (pattern == "BGGR")
            pattern = "GRBG";
        else return false;
    }

    if (pattern == "RGGB")
        debayerParams.filter = DC1394_COLOR_FILTER_RGGB;
    else if (pattern == "GBRG")
        debayerParams.filter = DC1394_COLOR_FILTER_GBRG;
    else if (pattern == "GRBG")
        debayerParams.filter = DC1394_COLOR_FILTER_GRBG;
    else if (pattern == "BGGR")
        debayerParams.filter = DC1394_COLOR_FILTER_BGGR;
    // We return unless we find a valid pattern
    else
    {
        return false;
    }

    fits_read_key(fptr, TINT, "XBAYROFF", &debayerParams.offsetX, nullptr, &status);
    fits_read_key(fptr, TINT, "YBAYROFF", &debayerParams.offsetY, nullptr, &status);

    if (debayerParams.offsetX == 1)
    {
        // This may leave odd values in the 0th column if the color filter is not there
        // in the sensor, but otherwise should process the offset correctly.
        // Only offsets of 0 or 1 are implemented in debayer_8bit() and debayer_16bit().
        switch (debayerParams.filter)
        {
            case DC1394_COLOR_FILTER_RGGB:
                debayerParams.filter = DC1394_COLOR_FILTER_GRBG;
                break;
            case DC1394_COLOR_FILTER_GBRG:
                debayerParams.filter = DC1394_COLOR_FILTER_BGGR;
                break;
            case DC1394_COLOR_FILTER_GRBG:
                debayerParams.filter = DC1394_COLOR_FILTER_RGGB;
                break;
            case DC1394_COLOR_FILTER_BGGR:
                debayerParams.filter = DC1394_COLOR_FILTER_GBRG;
                break;
        }
        debayerParams.offsetX = 0;
    }
    if (debayerParams.offsetX != 0 || debayerParams.offsetY > 1 || debayerParams.offsetY < 0)
    {
        return false;
    }

    HasDebayer = true;

    return true;
}

void FITSData::getBayerParams(BayerParams * param)
{
    param->method  = debayerParams.method;
    param->filter  = debayerParams.filter;
    param->offsetX = debayerParams.offsetX;
    param->offsetY = debayerParams.offsetY;
}

void FITSData::setBayerParams(BayerParams * param)
{
    debayerParams.method  = param->method;
    debayerParams.filter  = param->filter;
    debayerParams.offsetX = param->offsetX;
    debayerParams.offsetY = param->offsetY;
}

bool FITSData::debayer(bool reload)
{
    if (reload)
    {
        int anynull = 0, status = 0;

        if (fits_read_img(fptr, m_Statistics.dataType, 1, m_Statistics.samples_per_channel, nullptr, m_ImageBuffer,
                          &anynull, &status))
        {
            //                char errmsg[512];
            //                fits_get_errstatus(status, errmsg);
            //                KSNotification::error(i18n("Error reading image: %1", QString(errmsg)), i18n("Debayer error"));
            return false;
        }
    }

    switch (m_Statistics.dataType)
    {
        case TBYTE:
            return debayer_8bit();

        case TUSHORT:
            return debayer_16bit();

        default:
            return false;
    }
}

bool FITSData::debayer_8bit()
{
    dc1394error_t error_code;

    uint32_t rgb_size = m_Statistics.samples_per_channel * 3 * m_Statistics.bytesPerPixel;
    uint8_t * destinationBuffer = nullptr;

    try
    {
        destinationBuffer = new uint8_t[rgb_size];
    }
    catch (const std::bad_alloc &e)
    {
        logOOMError(rgb_size);
        return false;
    }

    auto * bayer_source_buffer      = reinterpret_cast<uint8_t *>(m_ImageBuffer);
    auto * bayer_destination_buffer = reinterpret_cast<uint8_t *>(destinationBuffer);

    if (bayer_destination_buffer == nullptr)
    {
        logOOMError(rgb_size);
        return false;
    }

    int ds1394_height = m_Statistics.height;
    auto dc1394_source = bayer_source_buffer;

    if (debayerParams.offsetY == 1)
    {
        dc1394_source += m_Statistics.width;
        ds1394_height--;
    }
    // offsetX == 1 is handled in checkDebayer() and should be 0 here.

    error_code = dc1394_bayer_decoding_8bit(dc1394_source, bayer_destination_buffer, m_Statistics.width, ds1394_height,
                                            debayerParams.filter,
                                            debayerParams.method);

    if (error_code != DC1394_SUCCESS)
    {
        m_Statistics.channels = 1;
        delete[] destinationBuffer;
        return false;
    }

    if (m_ImageBufferSize != rgb_size)
    {
        delete[] m_ImageBuffer;
        try
        {
            m_ImageBuffer = new uint8_t[rgb_size];
        }
        catch (const std::bad_alloc &e)
        {
            delete[] destinationBuffer;
            logOOMError(rgb_size);
            return false;
        }

        m_ImageBufferSize = rgb_size;
    }

    auto bayered_buffer = reinterpret_cast<uint8_t *>(m_ImageBuffer);

    // Data in R1G1B1, we need to copy them into 3 layers for FITS

    uint8_t * rBuff = bayered_buffer;
    uint8_t * gBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height);
    uint8_t * bBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height * 2);

    int imax = m_Statistics.samples_per_channel * 3 - 3;
    for (int i = 0; i <= imax; i += 3)
    {
        *rBuff++ = bayer_destination_buffer[i];
        *gBuff++ = bayer_destination_buffer[i + 1];
        *bBuff++ = bayer_destination_buffer[i + 2];
    }

    // TODO Maybe all should be treated the same
    // Doing single channel saves lots of memory though for non-essential
    // frames
    m_Statistics.channels = (m_Mode == FITS_NORMAL || m_Mode == FITS_CALIBRATE) ? 3 : 1;
    m_Statistics.dataType = TBYTE;
    delete[] destinationBuffer;
    return true;
}

bool FITSData::debayer_16bit()
{
    dc1394error_t error_code;

    uint32_t rgb_size = m_Statistics.samples_per_channel * 3 * m_Statistics.bytesPerPixel;
    uint8_t *destinationBuffer = nullptr;
    try
    {
        destinationBuffer = new uint8_t[rgb_size];
    }
    catch (const std::bad_alloc &e)
    {
        logOOMError(rgb_size);
        return false;
    }

    auto * bayer_source_buffer      = reinterpret_cast<uint16_t *>(m_ImageBuffer);
    auto * bayer_destination_buffer = reinterpret_cast<uint16_t *>(destinationBuffer);

    if (bayer_destination_buffer == nullptr)
    {
        logOOMError(rgb_size);
        return false;
    }

    int ds1394_height = m_Statistics.height;
    auto dc1394_source = bayer_source_buffer;

    if (debayerParams.offsetY == 1)
    {
        dc1394_source += m_Statistics.width;
        ds1394_height--;
    }
    // offsetX == 1 is handled in checkDebayer() and should be 0 here.

    error_code = dc1394_bayer_decoding_16bit(dc1394_source, bayer_destination_buffer, m_Statistics.width, ds1394_height,
                 debayerParams.filter,
                 debayerParams.method, 16);

    if (error_code != DC1394_SUCCESS)
    {
        m_Statistics.channels = 1;
        delete[] destinationBuffer;
        return false;
    }

    if (m_ImageBufferSize != rgb_size)
    {
        delete[] m_ImageBuffer;
        try
        {
            m_ImageBuffer = new uint8_t[rgb_size];
        }
        catch (const std::bad_alloc &e)
        {
            logOOMError(rgb_size);
            delete[] destinationBuffer;
            return false;
        }

        m_ImageBufferSize = rgb_size;
    }

    auto bayered_buffer = reinterpret_cast<uint16_t *>(m_ImageBuffer);

    // Data in R1G1B1, we need to copy them into 3 layers for FITS

    uint16_t * rBuff = bayered_buffer;
    uint16_t * gBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height);
    uint16_t * bBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height * 2);

    int imax = m_Statistics.samples_per_channel * 3 - 3;
    for (int i = 0; i <= imax; i += 3)
    {
        *rBuff++ = bayer_destination_buffer[i];
        *gBuff++ = bayer_destination_buffer[i + 1];
        *bBuff++ = bayer_destination_buffer[i + 2];
    }

    m_Statistics.channels = (m_Mode == FITS_NORMAL || m_Mode == FITS_CALIBRATE) ? 3 : 1;
    m_Statistics.dataType = TUSHORT;
    delete[] destinationBuffer;
    return true;
}

void FITSData::logOOMError(uint32_t requiredMemory)
{
}

double FITSData::getADU() const
{
    double adu = 0;
    for (int i = 0; i < m_Statistics.channels; i++)
        adu += m_Statistics.mean[i];

    return (adu / static_cast<double>(m_Statistics.channels));
}

QString FITSData::getLastError() const
{
    return m_LastError;
}

template <typename T>
void FITSData::convertToQImage(double dataMin, double dataMax, double scale, double zero, QImage &image)
{
    const auto * buffer = reinterpret_cast<const T *>(getImageBuffer());
    const T limit   = std::numeric_limits<T>::max();
    T bMin    = dataMin < 0 ? 0 : dataMin;
    T bMax    = dataMax > limit ? limit : dataMax;
    uint16_t w    = width();
    uint16_t h    = height();
    uint32_t size = w * h;
    double val;

    if (channels() == 1)
    {
        /* Fill in pixel values using indexed map, linear scale */
        for (int j = 0; j < h; j++)
        {
            unsigned char * scanLine = image.scanLine(j);

            for (int i = 0; i < w; i++)
            {
                val         = qBound(bMin, buffer[j * w + i], bMax);
                val         = val * scale + zero;
                scanLine[i] = qBound<unsigned char>(0, static_cast<uint8_t>(val), 255);
            }
        }
    }
    else
    {
        double rval = 0, gval = 0, bval = 0;
        QRgb value;
        /* Fill in pixel values using indexed map, linear scale */
        for (int j = 0; j < h; j++)
        {
            auto * scanLine = reinterpret_cast<QRgb *>((image.scanLine(j)));

            for (int i = 0; i < w; i++)
            {
                rval = qBound(bMin, buffer[j * w + i], bMax);
                gval = qBound(bMin, buffer[j * w + i + size], bMax);
                bval = qBound(bMin, buffer[j * w + i + size * 2], bMax);

                value = qRgb(rval * scale + zero, gval * scale + zero, bval * scale + zero);

                scanLine[i] = value;
            }
        }
    }
}

QImage FITSData::FITSToImage(const QString &filename, int maxWidth, int quality)
{
    QImage fitsImage;
    double min, max;

    FITSData data;

    QFuture<bool> future = data.loadFromFile(filename);

    // Wait synchronously
    future.waitForFinished();
    if (future.result() == false)
        return fitsImage;

    data.getMinMax(&min, &max);

    auto width = data.width();
    auto height = data.height();
    auto channels = data.channels();
    auto dataType = data.dataType();

    if (min == max)
    {
        fitsImage.fill(Qt::white);
        if (maxWidth > 0 && width > maxWidth)
            fitsImage = fitsImage.scaledToWidth(maxWidth, quality > 50 ? Qt::SmoothTransformation : Qt::FastTransformation);
        return fitsImage;
    }

    if (channels == 1)
    {
        fitsImage = QImage(width, height, QImage::Format_Indexed8);

        fitsImage.setColorCount(256);
        for (int i = 0; i < 256; i++)
            fitsImage.setColor(i, qRgb(i, i, i));
    }
    else
    {
        fitsImage = QImage(width, height, QImage::Format_RGB32);
    }

    Stretch stretch(width, height, channels, dataType);

    // Compute new auto-stretch params.
    auto stretchParams = stretch.computeParams(data.getImageBuffer());
    stretch.setParams(stretchParams);
    stretch.run(data.getImageBuffer(), &fitsImage, 1);

    // Resize if necessary
    if (maxWidth > 0 && width > maxWidth)
        fitsImage = fitsImage.scaledToWidth(maxWidth, quality > 50 ? Qt::SmoothTransformation : Qt::FastTransformation);

    return fitsImage;
}

bool FITSData::ImageToFITS(const QString &filename, const QString &format, QString &output)
{
    if (QImageReader::supportedImageFormats().contains(format.toLatin1()) == false)
    {
        return false;
    }

    QImage input;

    if (input.load(filename, format.toLatin1()) == false)
    {
        return false;
    }

    output = QDir(getTemporaryPath()).filePath(filename + ".fits");

    //This section sets up the FITS File
    fitsfile *fptr = nullptr;
    int status = 0;
    long  fpixel = 1, naxis = input.allGray() ? 2 : 3, nelements, exposure;
    long naxes[3] = { input.width(), input.height(), naxis == 3 ? 3 : 1 };
    char error_status[512] = {0};

    if (fits_create_file(&fptr, QString('!' + output).toLocal8Bit().data(), &status))
    {
        fits_get_errstatus(status, error_status);
        return false;
    }

    if (fits_create_img(fptr, BYTE_IMG, naxis, naxes, &status))
    {
        status = 0;
        fits_flush_file(fptr, &status);
        fits_close_file(fptr, &status);
        return false;
    }

    exposure = 1;
    fits_update_key(fptr, TLONG, "EXPOSURE", &exposure, "Total Exposure Time", &status);

    // Gray image
    if (naxis == 2)
    {
        nelements = naxes[0] * naxes[1];
        if (fits_write_img(fptr, TBYTE, fpixel, nelements, input.bits(), &status))
        {
            fits_get_errstatus(status, error_status);
            status = 0;
            fits_flush_file(fptr, &status);
            fits_close_file(fptr, &status);
            return false;
        }
    }
    // RGB image, we have to convert from ARGB format to R G B for each plane
    else
    {
        nelements = naxes[0] * naxes[1] * 3;

        uint8_t *srcBuffer = input.bits();
        // ARGB
        uint32_t srcBytes = naxes[0] * naxes[1] * 4 - 4;

        uint8_t *rgbBuffer = new uint8_t[nelements];
        if (rgbBuffer == nullptr)
        {
            fits_flush_file(fptr, &status);
            fits_close_file(fptr, &status);
            return false;
        }

        uint8_t *subR = rgbBuffer;
        uint8_t *subG = rgbBuffer + naxes[0] * naxes[1];
        uint8_t *subB = rgbBuffer + naxes[0] * naxes[1] * 2;
        for (uint32_t i = 0; i < srcBytes; i += 4)
        {
            *subB++ = srcBuffer[i];
            *subG++ = srcBuffer[i + 1];
            *subR++ = srcBuffer[i + 2];
        }

        if (fits_write_img(fptr, TBYTE, fpixel, nelements, rgbBuffer, &status))
        {
            fits_get_errstatus(status, error_status);
            status = 0;
            fits_flush_file(fptr, &status);
            fits_close_file(fptr, &status);
            delete [] rgbBuffer;
            return false;
        }

        delete [] rgbBuffer;
    }

    if (fits_flush_file(fptr, &status))
    {
        fits_get_errstatus(status, error_status);
        status = 0;
        fits_close_file(fptr, &status);
        return false;
    }

    if (fits_close_file(fptr, &status))
    {
        fits_get_errstatus(status, error_status);
        return false;
    }

    return true;
}

void FITSData::injectWCS(double orientation, double ra, double dec, double pixscale, bool eastToTheRight)
{
    int status = 0;

    if (fptr == nullptr)
        return;

    fits_update_key(fptr, TDOUBLE, "OBJCTRA", &ra, "Object RA", &status);
    fits_update_key(fptr, TDOUBLE, "OBJCTDEC", &dec, "Object DEC", &status);

    int epoch = 2000;

    fits_update_key(fptr, TINT, "EQUINOX", &epoch, "Equinox", &status);

    fits_update_key(fptr, TDOUBLE, "CRVAL1", &ra, "CRVAL1", &status);
    fits_update_key(fptr, TDOUBLE, "CRVAL2", &dec, "CRVAL1", &status);

    char radecsys[8] = "FK5";
    char ctype1[16]  = "RA---TAN";
    char ctype2[16]  = "DEC--TAN";

    fits_update_key(fptr, TSTRING, "RADECSYS", radecsys, "RADECSYS", &status);
    fits_update_key(fptr, TSTRING, "CTYPE1", ctype1, "CTYPE1", &status);
    fits_update_key(fptr, TSTRING, "CTYPE2", ctype2, "CTYPE2", &status);

    double crpix1 = width() / 2.0;
    double crpix2 = height() / 2.0;

    fits_update_key(fptr, TDOUBLE, "CRPIX1", &crpix1, "CRPIX1", &status);
    fits_update_key(fptr, TDOUBLE, "CRPIX2", &crpix2, "CRPIX2", &status);

    // Arcsecs per Pixel
    double secpix1 = eastToTheRight ? pixscale : -pixscale;
    double secpix2 = pixscale;

    fits_update_key(fptr, TDOUBLE, "SECPIX1", &secpix1, "SECPIX1", &status);
    fits_update_key(fptr, TDOUBLE, "SECPIX2", &secpix2, "SECPIX2", &status);

    double degpix1 = secpix1 / 3600.0;
    double degpix2 = secpix2 / 3600.0;

    fits_update_key(fptr, TDOUBLE, "CDELT1", &degpix1, "CDELT1", &status);
    fits_update_key(fptr, TDOUBLE, "CDELT2", &degpix2, "CDELT2", &status);

    // Rotation is CW, we need to convert it to CCW per CROTA1 definition
    double rotation = 360 - orientation;
    if (rotation > 360)
        rotation -= 360;

    fits_update_key(fptr, TDOUBLE, "CROTA1", &rotation, "CROTA1", &status);
    fits_update_key(fptr, TDOUBLE, "CROTA2", &rotation, "CROTA2", &status);

    m_WCSState = Idle;
}

bool FITSData::contains(const QPointF &point) const
{
    return (point.x() >= 0 && point.y() >= 0 && point.x() <= m_Statistics.width && point.y() <= m_Statistics.height);
}

void FITSData::saveStatistics(FITSImage::Statistic &other)
{
    other = m_Statistics;
}

void FITSData::restoreStatistics(FITSImage::Statistic &other)
{
    m_Statistics = other;

    emit dataChanged();
}

void FITSData::constructHistogram()
{
    switch (m_Statistics.dataType)
    {
        case TBYTE:
            constructHistogramInternal<uint8_t>();
            break;

        case TSHORT:
            constructHistogramInternal<int16_t>();
            break;

        case TUSHORT:
            constructHistogramInternal<uint16_t>();
            break;

        case TLONG:
            constructHistogramInternal<int32_t>();
            break;

        case TULONG:
            constructHistogramInternal<uint32_t>();
            break;

        case TFLOAT:
            constructHistogramInternal<float>();
            break;

        case TLONGLONG:
            constructHistogramInternal<int64_t>();
            break;

        case TDOUBLE:
            constructHistogramInternal<double>();
            break;

        default:
            break;
    }
}

template <typename T> int32_t FITSData::histogramBinInternal(T value, int channel) const
{
    return qMax(static_cast<T>(0), qMin(static_cast<T>(m_HistogramBinCount),
                                        static_cast<T>(rint((value - m_Statistics.min[channel]) / m_HistogramBinWidth[channel]))));
}

template <typename T> int32_t FITSData::histogramBinInternal(int x, int y, int channel) const
{
    if (!m_ImageBuffer || !isHistogramConstructed())
        return 0;
    uint32_t samples = m_Statistics.width * m_Statistics.height;
    uint32_t offset = channel * samples;
    auto * const buffer = reinterpret_cast<T const *>(m_ImageBuffer);
    int index = y * m_Statistics.width + x;
    const T &sample = buffer[index + offset];
    return histogramBinInternal(sample, channel);
}

int32_t FITSData::histogramBin(int x, int y, int channel) const
{
    switch (m_Statistics.dataType)
    {
        case TBYTE:
            return histogramBinInternal<uint8_t>(x, y, channel);
            break;

        case TSHORT:
            return histogramBinInternal<int16_t>(x, y, channel);
            break;

        case TUSHORT:
            return histogramBinInternal<uint16_t>(x, y, channel);
            break;

        case TLONG:
            return histogramBinInternal<int32_t>(x, y, channel);
            break;

        case TULONG:
            return histogramBinInternal<uint32_t>(x, y, channel);
            break;

        case TFLOAT:
            return histogramBinInternal<float>(x, y, channel);
            break;

        case TLONGLONG:
            return histogramBinInternal<int64_t>(x, y, channel);
            break;

        case TDOUBLE:
            return histogramBinInternal<double>(x, y, channel);
            break;

        default:
            return 0;
            break;
    }
}

template <typename T> void FITSData::constructHistogramInternal()
{
    auto * const buffer = reinterpret_cast<T const *>(m_ImageBuffer);
    uint32_t samples = m_Statistics.width * m_Statistics.height;
    const uint32_t sampleBy = samples > 500000 ? samples / 500000 : 1;

    m_HistogramBinCount = qMax(0., qMin(m_Statistics.max[0] - m_Statistics.min[0], 256.0));
    if (m_HistogramBinCount <= 0)
        m_HistogramBinCount = 256;

    for (int n = 0; n < m_Statistics.channels; n++)
    {
        m_HistogramIntensity[n].fill(0, m_HistogramBinCount + 1);
        m_HistogramFrequency[n].fill(0, m_HistogramBinCount + 1);
        m_CumulativeFrequency[n].fill(0, m_HistogramBinCount + 1);
        // Distinguish between 0-1.0 ranges and ranges with integer values.
        const double minBinSize = (m_Statistics.max[n] > 1.1) ? 1.0 : .0001;
        m_HistogramBinWidth[n] = qMax(minBinSize, (m_Statistics.max[n] - m_Statistics.min[n]) / m_HistogramBinCount);
    }

    QVector<QFuture<void >> futures;

    for (int n = 0; n < m_Statistics.channels; n++)
    {
        futures.append(QtConcurrent::run([ = ]()
        {
            for (int i = 0; i < m_HistogramBinCount; i++)
                m_HistogramIntensity[n][i] = m_Statistics.min[n] + (m_HistogramBinWidth[n] * i);
        }));
    }

    for (int n = 0; n < m_Statistics.channels; n++)
    {
        futures.append(QtConcurrent::run([ = ]()
        {
            uint32_t offset = n * samples;

            for (uint32_t i = 0; i < samples; i += sampleBy)
            {
                int32_t id = histogramBinInternal<T>(buffer[i + offset], n);
                m_HistogramFrequency[n][id] += sampleBy;
            }
        }));
    }

    for (QFuture<void> future : futures)
        future.waitForFinished();

    futures.clear();

    for (int n = 0; n < m_Statistics.channels; n++)
    {
        futures.append(QtConcurrent::run([ = ]()
        {
            uint32_t accumulator = 0;
            for (int i = 0; i < m_HistogramBinCount; i++)
            {
                accumulator += m_HistogramFrequency[n][i];
                m_CumulativeFrequency[n].replace(i, accumulator);
            }
        }));
    }

    for (QFuture<void> future : futures)
        future.waitForFinished();

    futures.clear();

    // Custom index to indicate the overall contrast of the image
    if (m_CumulativeFrequency[RED_CHANNEL][m_HistogramBinCount / 4] > 0)
        m_JMIndex = m_CumulativeFrequency[RED_CHANNEL][m_HistogramBinCount / 8] / static_cast<double>
                    (m_CumulativeFrequency[RED_CHANNEL][m_HistogramBinCount /
                        4]);
    else
        m_JMIndex = 1;

    m_HistogramConstructed = true;
    emit histogramReady();
}

void FITSData::recordLastError(int errorCode)
{
    char fitsErrorMessage[512] = {0};
    fits_get_errstatus(errorCode, fitsErrorMessage);
    m_LastError = fitsErrorMessage;
}

double FITSData::getAverageMean(bool roi) const
{
    const FITSImage::Statistic* ptr = (roi ? &m_ROIStatistics : &m_Statistics);
    if (ptr->channels == 1)
        return ptr->mean[0];
    else
        return (ptr->mean[0] + ptr->mean[1] + ptr->mean[2]) / 3.0;
}

double FITSData::getAverageMedian(bool roi) const
{
    const FITSImage::Statistic* ptr = (roi ? &m_ROIStatistics : &m_Statistics);
    if (ptr->channels == 1)
        return ptr->median[0];
    else
        return (ptr->median[0] + ptr->median[1] + ptr->median[2]) / 3.0;
}

double FITSData::getAverageStdDev(bool roi) const
{
    const FITSImage::Statistic* ptr = (roi ? &m_ROIStatistics : &m_Statistics);
    if (ptr->channels == 1)
        return ptr->stddev[0];
    else
        return (ptr->stddev[0] + ptr->stddev[1] + ptr->stddev[2]) / 3.0;
}

QByteArray FITSData::metadata()
{
    QJsonObject meta;

    // Filename only without path
    QString filepath = filename();
    QFileInfo info = QFileInfo(filepath);
    QString filenameOnly = info.fileName();

    meta.insert("bscale", 1);
    meta.insert("bzero", 32768);
    meta.insert("naxis", 2);
    meta.insert("exptime", 1);
    meta.insert("xbinning", 1);
    meta.insert("ybinning", 1);

    QString frame = "Light";
    if (filepath.contains("Dark/"))
        frame = "Dark";
    else if (filepath.contains("Flat/"))
        frame = "Flat";
    else if (filepath.contains("Bias/"))
        frame = "Bias";

    meta.insert("frame", frame);

#if QT_VERSION >= QT_VERSION_CHECK(5,10,0)
    QDateTime fileTS = info.birthTime();
    if (!fileTS.isValid())
    {
        fileTS = info.metadataChangeTime();
        if (!fileTS.isValid())
            fileTS = QDateTime::currentDateTime();
    }
#else
    QDateTime fileTS = info.created();
    if (!fileTS.isValid())
        fileTS = QDateTime::currentDateTime();
#endif
    meta.insert("date-obs", fileTS.toString(Qt::ISODate));

    // Skip empty or useless metadata
    for (const auto &oneRecord : getRecords())
    {
        if (oneRecord.key == "EXTEND" || oneRecord.key == "SIMPLE" || oneRecord.key == "COMMENT" ||
                oneRecord.key.isEmpty() || oneRecord.value.toString().isEmpty())
            continue;
        meta.insert(oneRecord.key.toLower(), QJsonValue::fromVariant(oneRecord.value));
    }

    QString m_UUID = QUuid::createUuid().toString();
    m_UUID = m_UUID.remove(QRegularExpression("[-{}]"));

    // Add filename and size as wells
    meta.insert("uuid", m_UUID);
    meta.insert("filename", filenameOnly);
    meta.insert("filesize", static_cast<int>(size()));
    meta.insert("Content-Disposition", QString("attachment;filename=%1").arg(filenameOnly));

    return QJsonDocument(meta).toJson(QJsonDocument::Compact);
}
