/*
 * Inscoper NBO Example: Describe an Acquired Image
 * ================================================
 * This example demonstrates how to describe an acquired dataset in the NBO model:
 * image dimensions, pixel calibration, channels, and per-plane timing.
 *
 * Prerequisites:
 * -------------
 * 1. Installation: the include directory of the release must be on the include
 *    paths, and the application must link against the NBO library.
 */

#include <NBO/NBO/Channel.h>
#include <NBO/NBO/ChannelContrastMethod_Type.h>
#include <NBO/NBO/ChannelID_Type.h>
#include <NBO/NBO/Denomination_Type.h>
#include <NBO/NBO/Image.h>
#include <NBO/NBO/ImageID_Type.h>
#include <NBO/NBO/ImagingMethodList_Type.h>
#include <NBO/NBO/LSID_Type.h>
#include <NBO/NBO/LightPath.h>
#include <NBO/NBO/MetadataOnly.h>
#include <NBO/NBO/NonNegativeInt_Type.h>
#include <NBO/NBO/PixelType_Type.h>
#include <NBO/NBO/Pixels.h>
#include <NBO/NBO/PixelsDimensionOrder_Type.h>
#include <NBO/NBO/PixelsID_Type.h>
#include <NBO/NBO/Plane.h>
#include <NBO/NBO/PlaneID_Type.h>
#include <NBO/NBO/PositiveFloat_Type.h>
#include <NBO/NBO/PositiveInt_Type.h>
#include <NBO/NBO/StageLabel.h>

#include <filesystem>
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <tuple>
#include <vector>

using namespace Inscoper;

// --8<-- [start:main_logic]
// Values that would come from the acquisition software
constexpr int SIZE_X = 2304;
constexpr int SIZE_Y = 2304;
constexpr int SIZE_Z = 5;
constexpr int SIZE_C = 2;
constexpr int SIZE_T = 3;
constexpr float PIXEL_SIZE_XY = 0.108f;  // micrometers
constexpr float Z_STEP = 0.5f;           // micrometers

int main() {
    // Step 1: Describe the pixel block
    NBO::PixelsPtr pixels = std::make_shared<NBO::Pixels>();

    NBO::PixelsID_TypePtr pixelsId = std::make_shared<NBO::PixelsID_Type>();
    pixelsId->set("Pixels:1");
    pixels->setID(pixelsId);

    NBO::PixelsDimensionOrder_TypePtr dimensionOrder =
        std::make_shared<NBO::PixelsDimensionOrder_Type>();
    dimensionOrder->set("XYZTC");
    pixels->setDimensionOrder(dimensionOrder);

    const std::vector<std::pair<std::function<void(const NBO::PositiveInt_TypePtr &)>, int>> sizes =
        {
            {[&](const NBO::PositiveInt_TypePtr &v) { pixels->setSizeX(v); }, SIZE_X},
            {[&](const NBO::PositiveInt_TypePtr &v) { pixels->setSizeY(v); }, SIZE_Y},
            {[&](const NBO::PositiveInt_TypePtr &v) { pixels->setSizeZ(v); }, SIZE_Z},
            {[&](const NBO::PositiveInt_TypePtr &v) { pixels->setSizeC(v); }, SIZE_C},
            {[&](const NBO::PositiveInt_TypePtr &v) { pixels->setSizeT(v); }, SIZE_T},
        };

    for (const auto &[setter, value] : sizes) {
        NBO::PositiveInt_TypePtr size = std::make_shared<NBO::PositiveInt_Type>();
        size->set(value);
        setter(size);
    }

    NBO::PixelType_TypePtr pixelType = std::make_shared<NBO::PixelType_Type>();
    pixelType->set("uint16");
    pixels->setPixelType(pixelType);

    // Step 2: Add the physical calibration
    const std::vector<std::pair<std::function<void(const NBO::PositiveFloat_TypePtr &)>, float>>
        calibration = {
            {[&](const NBO::PositiveFloat_TypePtr &v) { pixels->setPhysicalSizeX(v); },
             PIXEL_SIZE_XY},
            {[&](const NBO::PositiveFloat_TypePtr &v) { pixels->setPhysicalSizeY(v); },
             PIXEL_SIZE_XY},
            {[&](const NBO::PositiveFloat_TypePtr &v) { pixels->setPhysicalSizeZ(v); }, Z_STEP},
        };

    for (const auto &[setter, value] : calibration) {
        NBO::PositiveFloat_TypePtr physicalSize = std::make_shared<NBO::PositiveFloat_Type>();
        physicalSize->set(value);
        setter(physicalSize);
    }

    // Step 3: Declare that the document carries metadata only
    // Pixels holds a choice between the binary alternatives (BinData, TiffData,
    // NGFFData) and MetadataOnly, used when the pixel data lives elsewhere.
    NBO::Pixels_ChoicePtr pixelsChoice = std::make_shared<NBO::Pixels_Choice>();
    std::vector<NBO::MetadataOnlyPtr> metadataOnlyList{std::make_shared<NBO::MetadataOnly>()};
    pixelsChoice->addMetadataOnly(metadataOnlyList);
    pixels->setpixels_choice(pixelsChoice);

    // Step 4: Describe the channels
    const std::vector<std::tuple<std::string, std::string, std::string>> channels = {
        {"DAPI", "Wide-field_Fluorescence", "Fluorescence"},
        {"Brightfield", "Transmitted_Bright-field", "Brightfield"},
    };

    int channelIndex = 0;
    for (const auto &[channelName, imagingMethod, contrast] : channels) {
        NBO::ChannelPtr channel = std::make_shared<NBO::Channel>();

        NBO::ChannelID_TypePtr channelId = std::make_shared<NBO::ChannelID_Type>();
        channelId->set("Channel:" + std::to_string(channelIndex));
        channel->setID(channelId);

        NBO::Denomination_TypePtr name = std::make_shared<NBO::Denomination_Type>();
        name->set(channelName);
        channel->setName(name);

        NBO::ImagingMethodList_TypePtr illuminationType =
            std::make_shared<NBO::ImagingMethodList_Type>();
        illuminationType->set(imagingMethod);
        channel->setIlluminationType(illuminationType);

        NBO::ChannelContrastMethod_TypePtr contrastMethod =
            std::make_shared<NBO::ChannelContrastMethod_Type>();
        contrastMethod->set(contrast);
        channel->setContrastMethod(contrastMethod);

        // The light path records the optics the channel was acquired through
        NBO::LightPathPtr lightPath = std::make_shared<NBO::LightPath>();
        NBO::LSID_TypePtr lightPathId = std::make_shared<NBO::LSID_Type>();
        lightPathId->set("LightPath:" + std::to_string(channelIndex));
        lightPath->setID(lightPathId);
        channel->setLightPath(lightPath);

        pixels->addToChannel_List(channel);
        ++channelIndex;
    }

    // Step 5: Record per-plane timing
    float timestamp = 0.0f;
    for (int theT = 0; theT < SIZE_T; ++theT) {
        for (int theC = 0; theC < SIZE_C; ++theC) {
            for (int theZ = 0; theZ < SIZE_Z; ++theZ) {
                NBO::PlanePtr plane = std::make_shared<NBO::Plane>();

                NBO::PlaneID_TypePtr planeId = std::make_shared<NBO::PlaneID_Type>();
                planeId->set("Plane:" + std::to_string(theT) + ":" + std::to_string(theC) + ":"
                             + std::to_string(theZ));
                plane->setID(planeId);

                const std::vector<
                    std::pair<std::function<void(const NBO::NonNegativeInt_TypePtr &)>, int>>
                    indices = {
                        {[&](const NBO::NonNegativeInt_TypePtr &v) { plane->setTheZ(v); }, theZ},
                        {[&](const NBO::NonNegativeInt_TypePtr &v) { plane->setTheC(v); }, theC},
                        {[&](const NBO::NonNegativeInt_TypePtr &v) { plane->setTheT(v); }, theT},
                    };

                for (const auto &[setter, value] : indices) {
                    NBO::NonNegativeInt_TypePtr indexValue =
                        std::make_shared<NBO::NonNegativeInt_Type>();
                    indexValue->set(value);
                    setter(indexValue);
                }

                plane->setTimestamp(timestamp);
                timestamp += 0.05f;

                pixels->addToPlane_List(plane);
            }
        }
    }

    // Step 6: Attach the pixel block to an image and export
    NBO::Image image;

    NBO::ImageID_TypePtr imageId = std::make_shared<NBO::ImageID_Type>();
    imageId->set("Image:1");
    image.setID(imageId);

    NBO::Denomination_TypePtr imageName = std::make_shared<NBO::Denomination_Type>();
    imageName->set("Well A1 - Field 1");
    image.setName(imageName);

    image.setPixels(pixels);

    NBO::StageLabelPtr stageLabel = std::make_shared<NBO::StageLabel>();
    stageLabel->setName("Well A1 - Field 1");
    stageLabel->setX(12500.0f);
    stageLabel->setY(8300.0f);
    stageLabel->setZ(4210.5f);
    image.addToStageLabel_List(stageLabel);

    std::filesystem::path outputPath =
        std::filesystem::temp_directory_path() / "image_metadata.xml";
    image.toXmlFile(outputPath.string());
    std::cout << "Image description written to " << outputPath.string() << std::endl;
    std::cout << "Planes: " << image.getPixels()->getPlane_List().size() << std::endl;
    std::cout << "Channels: ";
    for (const auto &channel : image.getPixels()->getChannel_List()) {
        std::cout << channel->getName()->get() << " ";
    }
    std::cout << std::endl;

    return 0;
}
// --8<-- [end:main_logic]
