//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Device/IO/ReadWriteTiff.cpp
//! @brief     Implements functions readTiff, writeTiff.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2018
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#ifdef BA_TIFF_SUPPORT

#include "Device/IO/ReadWriteTiff.h"
#include "Base/Axis/Frame.h"
#include "Base/Axis/MakeScale.h"
#include "Base/Axis/Scale.h"
#include "Base/Util/Assert.h"
#include "Base/Util/SysUtil.h"
#include "Device/Data/Datafield.h"
#include <cstring> // memcpy
#include <memory>
#include <sstream>
#include <tiffio.h>
#include <tiffio.hxx>

namespace {
const std::string ref_to_doc =
    "\n\nThe TIFF format requirements can be found in the web documentation.";
} // namespace

Datafield Util::RW::readTiff(std::istream& input_stream)
{
    // Update webdoc page about tiff requirements if they are changed.

    TIFF* tiffstream = TIFFStreamOpen("MemTIFF", &input_stream);
    if (!tiffstream)
        throw std::runtime_error("Cannot open the TIFF file" + ref_to_doc);

    //... Read header.

    std::ostringstream message;
    message << "Cannot read TIFF file:" << std::endl;

    uint32_t w, h;
    if (!TIFFGetField(tiffstream, TIFFTAG_IMAGEWIDTH, &w)
        || !TIFFGetField(tiffstream, TIFFTAG_IMAGELENGTH, &h)) {
        message << "missing width/height in header" << ref_to_doc << std::endl;
        throw std::runtime_error(message.str());
    }

    // BitsPerSample defaults to 1 according to the TIFF spec.
    uint16_t bitsPerSample;
    if (!TIFFGetField(tiffstream, TIFFTAG_BITSPERSAMPLE, &bitsPerSample))
        bitsPerSample = 1;

    if (8 != bitsPerSample && 16 != bitsPerSample && 32 != bitsPerSample) {
        message << "    TIFFTAG_BITSPERSAMPLE: " << bitsPerSample << std::endl
                << "Only 8, 16 or 32 bits per sample are allowed." << ref_to_doc << std::endl;
        throw std::runtime_error(message.str());
    }


    // they may be e.g. grayscale with 2 samples per pixel
    uint16_t samplesPerPixel;
    if (!TIFFGetField(tiffstream, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel))
        samplesPerPixel = 1;

    if (samplesPerPixel != 1) {
        message << "    TIFFTAG_SAMPLESPERPIXEL: " << samplesPerPixel << std::endl
                << "Only 1 sample per pixel (channel) is allowed." << ref_to_doc << std::endl;
        throw std::runtime_error(message.str());
    }


    uint16_t sampleFormat;
    if (!TIFFGetField(tiffstream, TIFFTAG_SAMPLEFORMAT, &sampleFormat))
        sampleFormat = 1;

    switch (sampleFormat) {
    case 1: // unsigned int
    case 2: // signed int
        break;
    case 3: // IEEE float
        if (32 != bitsPerSample) {
            message
                << "    TIFFTAG_BITSPERSAMPLE: " << bitsPerSample << std::endl
                << "    TIFFTAG_SAMPLEFORMAT: " << sampleFormat << std::endl
                << "Only 32 bits per sample are allowed for IEEE float format (sample format = 3)."
                << ref_to_doc << std::endl;
            throw std::runtime_error(message.str());
        }
        break;
    default:
        message << "    TIFFTAG_SAMPLEFORMAT: " << sampleFormat << std::endl
                << "Only value 1, 2 or 3 is allowed." << ref_to_doc << std::endl;
        throw std::runtime_error(message.str());
    }

    //... Read data.

    ASSERT(0 == bitsPerSample % 8);
    uint16_t bytesPerSample = bitsPerSample / 8;
    tmsize_t buf_size = TIFFScanlineSize(tiffstream);
    tmsize_t expected_size = bytesPerSample * w;
    if (buf_size != expected_size)
        throw std::runtime_error("Cannot read TIFF file: wrong scanline size" + ref_to_doc);

    tdata_t const buf = _TIFFmalloc(buf_size); // tdata_t is void*
    if (!buf)
        throw std::runtime_error("Cannot read TIFF file: failed allocating buffer" + ref_to_doc);

    Datafield data(std::vector<const Scale*>{newEquiDivision("u (bin)", w, 0.0, double(w)),
                                             newEquiDivision("v (bin)", h, 0.0, double(h))});

    std::vector<int8_t> line_buf;
    line_buf.resize(buf_size, 0);

    for (uint32_t row = 0; row < h; row++) {
        if (TIFFReadScanline(tiffstream, buf, row) < 0)
            throw std::runtime_error("Cannot read TIFF file: error in scanline." + ref_to_doc);

        memcpy(line_buf.data(), buf, buf_size);

        for (unsigned col = 0; col < w; ++col) {
            void* incoming = &line_buf[col * bytesPerSample];
            double sample = 0;

            switch (sampleFormat) {
            case 1: // unsigned int
                switch (bitsPerSample) {
                case 8:
                    sample = *reinterpret_cast<uint8_t*>(incoming);
                    break;
                case 16:
                    sample = *reinterpret_cast<uint16_t*>(incoming);
                    break;
                case 32:
                    sample = *reinterpret_cast<uint32_t*>(incoming);
                    break;
                default:
                    throw std::runtime_error("Corrupted TIFF file");
                }
                break;
            case 2: // signed int
                switch (bitsPerSample) {
                case 8:
                    sample = *reinterpret_cast<int8_t*>(incoming);
                    break;
                case 16:
                    sample = *reinterpret_cast<int16_t*>(incoming);
                    break;
                case 32:
                    sample = *reinterpret_cast<int32_t*>(incoming);
                    break;
                default:
                    throw std::runtime_error("Corrupted TIFF file");
                }
                break;
            case 3: // IEEE float
                sample = double(*reinterpret_cast<float*>(incoming));
                break;
            default:
                throw std::runtime_error("Cannot read TIFF file: unexpected sample format"
                                         + ref_to_doc);
            }

            data[(h - 1 - row) * w + col] = sample;
        }
    }
    _TIFFfree(buf);
    TIFFClose(tiffstream);

    return data;
}

void Util::RW::writeTiff(const Datafield& data, std::ostream& output_stream)
{
    if (data.rank() != 2)
        throw std::runtime_error("Cannot write TIFF file: unsupported data rank");
    TIFF* tiffstream = TIFFStreamOpen("MemTIFF", &output_stream);
    ASSERT(tiffstream);
    const size_t m_width = data.axis(0).size();
    const size_t m_height = data.axis(1).size(); // this does not exist for 1d data

    //... Write header.

    TIFFSetField(tiffstream, TIFFTAG_ARTIST, "BornAgain.IOFactory");
    TIFFSetField(tiffstream, TIFFTAG_DATETIME, Base::System::getCurrentDateAndTime().c_str());
    TIFFSetField(tiffstream, TIFFTAG_IMAGEDESCRIPTION,
                 "Image converted from BornAgain intensity file.");
    TIFFSetField(tiffstream, TIFFTAG_SOFTWARE, "BornAgain");

    const auto width = static_cast<uint32_t>(m_width);
    const auto height = static_cast<uint32_t>(m_height);
    TIFFSetField(tiffstream, TIFFTAG_IMAGEWIDTH, width);
    TIFFSetField(tiffstream, TIFFTAG_IMAGELENGTH, height);

    // output format, hardcoded here
    const uint16_t bitPerSample = 32;
    const uint16_t samplesPerPixel = 1;
    TIFFSetField(tiffstream, TIFFTAG_BITSPERSAMPLE, bitPerSample);
    TIFFSetField(tiffstream, TIFFTAG_SAMPLESPERPIXEL, samplesPerPixel);

    TIFFSetField(tiffstream, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISWHITE);

    //... Write data.

    using sample_t = int;
    const tmsize_t buf_size = sizeof(sample_t) * m_width;
    const tdata_t buf = _TIFFmalloc(buf_size);
    if (!buf)
        throw std::runtime_error("Cannot write TIFF file: failed allocating buffer");

    std::vector<sample_t> line_buf;
    line_buf.resize(m_width, 0);
    for (unsigned row = 0; row < (uint32_t)m_height; row++) {
        for (unsigned col = 0; col < line_buf.size(); ++col)
            line_buf[col] = static_cast<sample_t>(data[(m_height - 1 - row) * m_width + col]);
        memcpy(buf, line_buf.data(), buf_size);

        if (TIFFWriteScanline(tiffstream, buf, row) < 0)
            throw std::runtime_error("Cannot write TIFF file: error in TIFFWriteScanline");
    }
    _TIFFfree(buf);
    TIFFFlush(tiffstream);
    TIFFClose(tiffstream);
}

#endif // BA_TIFF_SUPPORT
