#include "loadimage.h"
#include "libxisf.h"
#include "rawimage.h"
#include <QDebug>
#include <QDir>
#include <QElapsedTimer>
#include <QFileInfo>
#include <libexif/exif-data.h>
#include <libraw/libraw.h>
#include "fitswrapper.h"

QString makeUNCPath(const QString &path)
{
#ifdef Q_OS_WIN64
    if(!path.startsWith("\\\\") && !path.startsWith("//"))
    {
        QString tmp;
        QFileInfo info(path);
        tmp = info.canonicalFilePath();
        tmp = QDir::toNativeSeparators(tmp);
        tmp.prepend("\\\\?\\");
        qDebug() << "makeMaxPath" << path << tmp;
        return tmp;
    }
#endif
    return path;
}

int loadFITSHeader(fitsfile *file, ImageInfoData &info)
{
    int imgtype;
    int naxis;
    long naxes[3] = {0};
    int nexist;
    int status = 0;
    char key[FLEN_KEYWORD];
    char val[FLEN_VALUE];
    char comm[FLEN_COMMENT];
    char strval[FLEN_VALUE];
    QVariant var;
    fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
    fits_get_hdrspace(file, &nexist, nullptr, &status);
    for(int i=1; i<=nexist; i++)
    {
        fits_read_keyn(file, i, key, val, comm, &status);
        fits_read_key(file, TSTRING, key, strval, nullptr, &status);
        if(status == 0 || status == VALUE_UNDEFINED)
        {
            QString string(strval);
            bool isint;
            bool isdouble;
            double vald = string.toDouble(&isdouble);
            long long vall = string.toLongLong(&isint);
            if(isint)
                var = vall;
            else if(isdouble)
                var = vald;
            else if(status == VALUE_UNDEFINED)
                var = QVariant();
            else if(string == "T" || string == "F")
                var = string == "T";
            else
                var = string;
            status = 0;
            info.fitsHeader.append(FITSRecord(key, var, comm));
        }
        else
        {
            return status;
        }
    }

    char *header = nullptr;
    int nrec = 0;
    const char *exclist[] = {"PV1_1", "PV1_2"};
    fits_hdr2str(file, TRUE, (char**)exclist, 2, &header, &nrec, &status);
    if(status == 0)
    {
        info.wcs = std::make_shared<WCSDataT>(naxes[0], naxes[1], header, nrec);
        if(!info.wcs->valid())info.wcs.reset();
    }
    fits_free_memory(header, &status);
    return status;
}

bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image, bool planar, uint32_t index)
{
    int status = 0;
    int num = 0;
    long naxes[3] = {0};

    auto checkError = [&info, &status]()
    {
        char err[100];
        fits_get_errstatus(status, err);
        info.info.append({QObject::tr("Error"), QString(err)});
        qWarning() << "Failed to load FITS file" << err;
        return false;
    };

    FITSWrapper file(path, READONLY, true);
    status = file.status();

    if(status)return checkError();
    fits_get_num_hdus(file, &num, &status);
    if(status)return checkError();

    int hdutype;
    int imgtype;
    int naxis;
    std::vector<int> imageIdxs;
    for(int i = 1; i <= num; i++)
    {
        fits_movabs_hdu(file, i, &hdutype, &status);if(status)return checkError();
        if(hdutype == IMAGE_HDU)
        {
            fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);if(status)return checkError();
            if(naxis >= 2 && naxis <= 3)imageIdxs.push_back(i);
        }
    }
    info.num = imageIdxs.size();
    info.index = index;

    if(index >= imageIdxs.size())return false;

    fits_movabs_hdu(file, imageIdxs[index], &hdutype, &status);if(status)return checkError();
    if(hdutype == IMAGE_HDU)
    {
        naxes[0] = naxes[1] = naxes[2] = 0;
        fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);if(status)return checkError();
        fits_get_img_equivtype(file, &imgtype, &status);if(status)return checkError();

        if(hdutype == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
        {
            RawImage::DataType type;
            int fitstype;
            long fpixel[3] = {1,1,1};
            switch(imgtype)
            {
            case BYTE_IMG:
                type = RawImage::UINT8;
                fitstype = TBYTE;
                break;
            case SHORT_IMG:
                type = RawImage::UINT16;
                fitstype = TSHORT;
                break;
            case USHORT_IMG:
                type = RawImage::UINT16;
                fitstype = TUSHORT;
                break;
            case LONG_IMG:
                type = RawImage::UINT32;
                fitstype = TINT;
                break;
            case ULONG_IMG:
                type = RawImage::UINT32;
                fitstype = TUINT;
                break;
            case FLOAT_IMG:
                type = RawImage::FLOAT32;
                fitstype = TFLOAT;
                break;
            case DOUBLE_IMG:
                type = RawImage::FLOAT64;
                fitstype = TDOUBLE;
                break;
            default:
                info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
                goto noload;
                break;
            }

            size_t size = naxes[0]*naxes[1];
            size_t w = naxes[0];
            size_t h = naxes[1];

            info.info.append({QObject::tr("Width"), QString::number(naxes[0])});
            info.info.append({QObject::tr("Height"), QString::number(naxes[1])});

            RawImage img(w, h, naxis == 2 ? 1 : naxes[2], type);
            uint8_t *data = static_cast<uint8_t*>(img.data());
            for (int i=1; i==1 || i<=naxes[2]; i++)
            {
                fpixel[2] = i;
                fits_read_pix(file, fitstype, fpixel, size, NULL, data + img.size() * RawImage::typeSize(type) * (i-1), NULL, &status);
                if(status)return checkError();
            }
            if(fitstype == TSHORT)
            {
                uint16_t *s = static_cast<uint16_t*>(img.data());
                size_t size = img.size() * img.channels();
                for(size_t i=0; i<size; i++)
                    s[i] -= INT16_MIN;
            }
            else if(fitstype == TINT)
            {
                uint32_t *s = static_cast<uint32_t*>(img.data());
                size_t size = img.size() * img.channels();
                for(size_t i=0; i<size; i++)
                    s[i] -= INT32_MIN;
            }

            if(img.channels() == 1 || planar)
                image = std::make_shared<RawImage>(std::move(img));
            else
                image = RawImage::fromPlanar(img);
        }
    }
noload:
    if(file)
    {
        status = loadFITSHeader(file, info);
        if(status)return checkError();
    }

    if(image)
    {
        for(auto fits : info.fitsHeader)
        {
            if(fits.key == "ROWORDER" && fits.value == "BOTTOM-UP")
                image->flip();
        }
    }

    return true;
}

bool loadXISF(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &image, bool planar, uint32_t index)
{
    try
    {
        LibXISF::XISFReader xisf;
        std::filesystem::path fspath(path.toLocal8Bit().data());
        xisf.open(fspath);

        if(index >= (uint32_t)xisf.imagesCount())return false;
        const LibXISF::Image &xisfImage = xisf.getImage(index);

        auto fitskeywords = xisfImage.fitsKeywords();
        for(auto fits : fitskeywords)
        {
            info.fitsHeader.append(fits);
        }

        QVector<FITSRecord> xisfWCS;
        auto imageproperties = xisfImage.imageProperties();
        for(auto prop : imageproperties)
        {
            info.fitsHeader.append(prop);
            if(prop.id == "PCL:AstrometricSolution:ReferenceCelestialCoordinates" && prop.value.type() == LibXISF::Variant::Type::F64Vector)
            {
                auto val = prop.value.value<LibXISF::F64Vector>();
                if(val.size() >= 2)
                {
                    xisfWCS.append({"CRVAL1", val[0], "value from PCL:AstrometricSolution"});
                    xisfWCS.append({"CRVAL2", val[1], "value from PCL:AstrometricSolution"});
                }
            }
            else if(prop.id == "PCL:AstrometricSolution:ReferenceImageCoordinates" && prop.value.type() == LibXISF::Variant::Type::F64Vector)
            {
                auto val = prop.value.value<LibXISF::F64Vector>();
                if(val.size() >= 2)
                {
                    xisfWCS.append({"CRPIX1", val[0], "value from PCL:AstrometricSolution"});
                    xisfWCS.append({"CRPIX2", val[1], "value from PCL:AstrometricSolution"});
                }
            }
            else if(prop.id == "PCL:AstrometricSolution:LinearTransformationMatrix" && prop.value.type() == LibXISF::Variant::Type::F64Matrix)
            {
                auto val = prop.value.value<LibXISF::F64Matrix>();
                if(val.cols() >= 2 && val.rows() >= 2)
                {
                    xisfWCS.append({"CD1_1", val(0, 0), "value from PCL:AstrometricSolution"});
                    xisfWCS.append({"CD1_2", val(0, 1), "value from PCL:AstrometricSolution"});
                    xisfWCS.append({"CD2_1", val(1, 0), "value from PCL:AstrometricSolution"});
                    xisfWCS.append({"CD2_2", val(1, 1), "value from PCL:AstrometricSolution"});
                }
            }
            else if(prop.id == "PCL:AstrometricSolution:ProjectionSystem")
            {
                if(prop.value.toString() == "Gnomonic")
                {
                    xisfWCS.append({"CTYPE1", "RA---TAN", "value from PCL:AstrometricSolution"});
                    xisfWCS.append({"CTYPE", "DEC--TAN", "value from PCL:AstrometricSolution"});
                }
            }
        }

        info.num = xisf.imagesCount();
        info.index = index;
        info.info.append({QObject::tr("Width"), QString::number(xisfImage.width())});
        info.info.append({QObject::tr("Height"), QString::number(xisfImage.height())});

        auto wcs = std::make_shared<WCSDataT>(xisfImage.width(), xisfImage.height(), info.fitsHeader);
        if(!wcs->valid() && xisfWCS.size())wcs = std::make_shared<WCSDataT>(xisfImage.width(), xisfImage.height(), xisfWCS);
        if(wcs->valid())info.wcs = wcs;

        RawImage::DataType type;
        switch(xisfImage.sampleFormat())
        {
        case LibXISF::Image::UInt8: type = RawImage::UINT8; break;
        case LibXISF::Image::UInt16: type = RawImage::UINT16; break;
        case LibXISF::Image::UInt32: type = RawImage::UINT32; break;
        case LibXISF::Image::Float32: type = RawImage::FLOAT32; break;
        case LibXISF::Image::Float64: type = RawImage::FLOAT64; break;
        default: break;
        }

        LibXISF::Image tmpImage = xisfImage;
        tmpImage.convertPixelStorageTo(LibXISF::Image::Planar);
        if(tmpImage.colorSpace() == LibXISF::Image::ColorSpace::Gray)
        {
            image = std::make_shared<RawImage>(tmpImage.width(), tmpImage.height(), 1, type);
            std::memcpy(image->data(), tmpImage.imageData(), tmpImage.imageDataSize() / tmpImage.channelCount());
            image->setICCProfile(tmpImage.iccProfile());
            return true;
        }
        else if(tmpImage.channelCount() == 3 || tmpImage.channelCount() == 4)
        {
            if(planar)
            {
                image = std::make_shared<RawImage>(tmpImage.width(), tmpImage.height(), tmpImage.channelCount(), type);
                std::memcpy(image->data(), tmpImage.imageData(), tmpImage.imageDataSize());
            }
            else
            {
                image = RawImage::fromPlanar(tmpImage.imageData(), tmpImage.width(), tmpImage.height(), tmpImage.channelCount(), type);
            }

            image->setICCProfile(tmpImage.iccProfile());
            return true;
        }
        return false;
    }
    catch (LibXISF::Error &err)
    {
        info.info.append(QPair<QString, QString>("Error", err.what()));
        qWarning() << "Failed to load XISF" << err.what();
        return false;
    }
    info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
    return false;
}

bool readFITSHeader(const QString &path, ImageInfoData &info)
{
    QString path2 = makeUNCPath(path);

    FITSWrapper file(path, READONLY, true);
    int status = file.status();

    if(file && status == 0)
    {
        int num = 0;
        int hdutype;
        int imgtype, naxis;
        long naxes[3] = {0};
        fits_get_num_hdus(file, &num, &status);if(status)return false;
        for(int i = 1; i <= num; i++)
        {
            fits_movabs_hdu(file, i, &hdutype, &status);if(status)return false;
            if(hdutype == IMAGE_HDU)
            {
                fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);if(status)return false;
                if(naxis >= 2 && naxis <= 3)break;
            }
        }
        status = loadFITSHeader(file, info);
    }
    return status == 0;
}

bool readXISFHeader(const QString &path, ImageInfoData &info)
{
    QString path2 = makeUNCPath(path);
    try
    {
        LibXISF::XISFReader xisf;
        std::filesystem::path fspath(path2.toLocal8Bit().data());
        xisf.open(fspath);
        const LibXISF::Image &image = xisf.getImage(0, false);

        auto fitskeywords = image.fitsKeywords();
        for(auto fits : fitskeywords)
        {
            info.fitsHeader.append(fits);
        }

        auto imageproperties = image.imageProperties();
        for(auto prop : imageproperties)
        {
            info.fitsHeader.append(prop);
        }
        info.wcs = std::make_shared<WCSDataT>(image.width(), image.height(), info.fitsHeader);
        if(!info.wcs->valid())info.wcs.reset();
    }
    catch (LibXISF::Error &err)
    {
        qWarning() << "LibXISF error" << err.what();
        return false;
    }
    return true;
}

void loadExifEntry(ImageInfoData &info, ExifContent *content, ExifTag tag)
{
    char val[1024];
    ExifEntry *entry = exif_content_get_entry(content, tag);
    if(entry)
    {
        exif_entry_get_value(entry, val, sizeof(val));
        info.info.append({exif_tag_get_title(tag), val});
    }
}

bool loadRAW(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image)
{
    std::unique_ptr<LibRaw> raw = std::make_unique<LibRaw>();
    raw->open_file(path.toLocal8Bit().data());
    raw->imgdata.params.half_size = true;
    raw->imgdata.params.use_camera_wb = true;
    raw->imgdata.params.user_flip = 0;
    if(raw->unpack())
        return false;


    libraw_rawdata_t rawdata = raw->imgdata.rawdata;
    size_t size = rawdata.sizes.width*rawdata.sizes.height;

    std::vector<uint16_t> out;
    out.resize(size);
    size_t d = 0;
    uint h=rawdata.sizes.top_margin+rawdata.sizes.height;
    uint w=rawdata.sizes.left_margin+rawdata.sizes.width;
    size_t pitch = rawdata.sizes.raw_pitch/sizeof(uint16_t);

    for(size_t i=rawdata.sizes.top_margin;i<h;i++)
    {
        for(size_t o=rawdata.sizes.left_margin;o<w;o++)
        {
            uint16_t p = rawdata.raw_image[i*pitch+o];
            out[d++] = p;
        }
    }
    image = std::make_shared<RawImage>(rawdata.sizes.width, rawdata.sizes.height, 1, RawImage::UINT16);
    memcpy(image->data(), &out[0], sizeof(uint16_t)*d);

    QString shutterSpeed = QString::number(raw->imgdata.other.shutter);
    if(raw->imgdata.other.shutter < 1)
    {
        shutterSpeed = QString("1/%1s").arg(1.0f/raw->imgdata.other.shutter);
    }
    info.info.append({QObject::tr("Width"), QString::number(raw->imgdata.sizes.width)});
    info.info.append({QObject::tr("Height"), QString::number(raw->imgdata.sizes.height)});
    info.info.append({QObject::tr("ISO"), QString::number(raw->imgdata.other.iso_speed)});
    info.info.append({QObject::tr("Shutter speed"), shutterSpeed});
#if LIBRAW_MINOR_VERSION>=19
//    info.append(StringPair(QObject::tr("Camera temperature"), QString::number(raw.imgdata.other.CameraTemperature)));
#endif
    return true;
}

bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage, int index, bool planar)
{
    bool ret = false;
    QElapsedTimer timer;
    QFileInfo fileInfo(path);
    timer.start();
    if(path.endsWith(".CR2", Qt::CaseInsensitive) || path.endsWith(".CR3", Qt::CaseInsensitive) || path.endsWith(".NEF", Qt::CaseInsensitive) || path.endsWith(".DNG", Qt::CaseInsensitive))
    {
        ret = loadRAW(path, info, rawImage);
        qDebug() << "LoadRAW" << timer.elapsed() << "ms";
    }
    else if(isFITS(fileInfo.suffix()))
    {
        ret = loadFITS(path, info, rawImage, planar, index);
        qDebug() << "LoadFITS" << timer.elapsed() << "ms";
    }
    else if(isXISF(fileInfo.suffix()))
    {
        ret = loadXISF(path, info, rawImage, planar, index);
        qDebug() << "LoadXISF" << timer.elapsed() << "ms";
    }
    else
    {
        QImage img(path);

        ExifData *exif = exif_data_new_from_file(path.toLocal8Bit().constData());
        info.info.append({QObject::tr("Width"), QString::number(img.width())});
        info.info.append({QObject::tr("Height"), QString::number(img.height())});
        if(exif)
        {
            loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_ISO_SPEED_RATINGS);
            loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_SHUTTER_SPEED_VALUE);
            exif_data_free(exif);
        }
        rawImage = std::make_shared<RawImage>(img);
        qDebug() << "LoadQImage" << timer.elapsed() << "ms";
        ret = !img.isNull();
    }
    return ret;
}

bool isFITS(const QString &suffix)
{
    return suffix.compare("fits", Qt::CaseInsensitive) == 0 || suffix.compare("fit", Qt::CaseInsensitive) == 0 || suffix.compare("fts", Qt::CaseInsensitive) == 0 || suffix.compare("fz", Qt::CaseInsensitive) == 0;
}

bool isXISF(const QString &suffix)
{
    return suffix.compare("xisf", Qt::CaseInsensitive) == 0;
}
