/*
 * Inscoper NBO Example: Describe an Instrument
 * ============================================
 * This example demonstrates how to describe a complete microscope in the NBO
 * model, from the stand to the light sources, and export the result as NBO-XML.
 *
 * 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/CMOS.h>
#include <NBO/NBO/CameraIllumination_Type.h>
#include <NBO/NBO/ContrastModulationPlate_Type.h>
#include <NBO/NBO/Denomination_Type.h>
#include <NBO/NBO/DetectorID_Type.h>
#include <NBO/NBO/DetectorNoiseModel_Type.h>
#include <NBO/NBO/DigitizerType_Type.h>
#include <NBO/NBO/IlluminationWavelengthRange.h>
#include <NBO/NBO/ImmersionTypeList_Type.h>
#include <NBO/NBO/Instrument.h>
#include <NBO/NBO/InstrumentID_Type.h>
#include <NBO/NBO/InvertedMicroscopeStand.h>
#include <NBO/NBO/LSID_Type.h>
#include <NBO/NBO/Laser.h>
#include <NBO/NBO/LaserModulation_Type.h>
#include <NBO/NBO/LaserType_Type.h>
#include <NBO/NBO/LightSourceID_Type.h>
#include <NBO/NBO/MicroscopeType_Type.h>
#include <NBO/NBO/Objective.h>
#include <NBO/NBO/ObjectiveCorrection_Type.h>
#include <NBO/NBO/ObjectiveID_Type.h>
#include <NBO/NBO/PositiveFloat_Type.h>
#include <NBO/NBO/UnitsLength_Type.h>

#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
#include <vector>

using namespace Inscoper;

// --8<-- [start:main_logic]
int main() {
    // Step 1: Create the instrument and identify it
    NBO::Instrument instrument;

    NBO::InstrumentID_TypePtr instrumentId = std::make_shared<NBO::InstrumentID_Type>();
    instrumentId->set("Instrument:1");
    instrument.setID(instrumentId);

    NBO::Denomination_TypePtr instrumentName = std::make_shared<NBO::Denomination_Type>();
    instrumentName->set("Nikon Ti2 - Room 214");
    instrument.setName(instrumentName);

    // Step 2: Add the microscope stand
    // MicroscopeStand is abstract: the concrete class states the geometry of the
    // stand, here an inverted one.
    NBO::InvertedMicroscopeStandPtr stand = std::make_shared<NBO::InvertedMicroscopeStand>();

    NBO::LSID_TypePtr standId = std::make_shared<NBO::LSID_Type>();
    standId->set("MicroscopeStand:1");
    stand->setID(standId);

    stand->setManufacturer("Nikon");
    stand->setModel("Eclipse Ti2-E");
    stand->setCatalogNumber("Ti2-E");

    NBO::MicroscopeType_TypePtr standType = std::make_shared<NBO::MicroscopeType_Type>();
    standType->set("Compound");
    stand->setType(standType);

    instrument.addToMicroscopeStandGroup_List(stand);

    // Step 3: Add the objectives
    // Objective declares twelve required attributes, so a compliant description of
    // a lens states its optical characteristics in full.
    struct ObjectiveSpec {
        std::string model;
        std::string catalogNumber;
        float magnification;
        float lensNA;
        std::string immersion;
        float workingDistance;
    };

    const std::vector<ObjectiveSpec> objectiveSpecs = {
        {"CFI Plan Apo Lambda 20x", "MRD00205", 20.0f, 0.75f, "Air", 1.0f},
        {"CFI Plan Apo Lambda 60x", "MRD01605", 60.0f, 1.4f, "Mineral Oil", 0.13f},
    };

    int index = 1;
    for (const auto &spec : objectiveSpecs) {
        NBO::ObjectivePtr objective = std::make_shared<NBO::Objective>();

        NBO::ObjectiveID_TypePtr objectiveId = std::make_shared<NBO::ObjectiveID_Type>();
        objectiveId->set("Objective:" + std::to_string(index));
        objective->setID(objectiveId);

        objective->setManufacturer("Nikon");
        objective->setModel(spec.model);
        objective->setCatalogNumber(spec.catalogNumber);

        objective->setMagnification(spec.magnification);
        objective->setLensNA(spec.lensNA);
        objective->setInfinityCorrected(true);
        objective->setDIC(false);
        objective->setCorrectionCollar(false);
        objective->setWorkingDistance(spec.workingDistance);
        objective->setObjectiveViewField(25.0f);
        objective->setImageDistance(200.0f);

        NBO::ObjectiveCorrection_TypePtr correction =
            std::make_shared<NBO::ObjectiveCorrection_Type>();
        correction->set("Apochromat");
        objective->setCorrection(correction);

        NBO::ImmersionTypeList_TypePtr immersionType =
            std::make_shared<NBO::ImmersionTypeList_Type>();
        immersionType->set(spec.immersion);
        objective->setImmersionType(immersionType);

        NBO::ContrastModulationPlate_TypePtr contrastModulation =
            std::make_shared<NBO::ContrastModulationPlate_Type>();
        contrastModulation->set("None");
        objective->setContrastModulation(contrastModulation);

        // Lengths carry their unit as a separate attribute
        NBO::UnitsLength_TypePtr millimeter = std::make_shared<NBO::UnitsLength_Type>();
        millimeter->set("mm");
        objective->setWorkingDistanceUnit(millimeter);

        instrument.addToObjective_List(objective);
        ++index;
    }

    // Step 4: Add the detector
    // CMOS derives from Camera, which derives from the abstract Detector, so a CMOS
    // instance is accepted by the detector group of the instrument.
    NBO::CMOSPtr camera = std::make_shared<NBO::CMOS>();

    NBO::DetectorID_TypePtr cameraId = std::make_shared<NBO::DetectorID_Type>();
    cameraId->set("Detector:1");
    camera->setID(cameraId);

    camera->setManufacturer("Hamamatsu");
    camera->setModel("ORCA-Fusion BT");
    camera->setCatalogNumber("C15440-20UP");

    camera->setPixelWidth(6.5f);
    camera->setPixelHeight(6.5f);
    camera->setArrayWidth(2304);
    camera->setArrayHeight(2304);
    camera->setPixelWellCapacity(15000);
    camera->setMaximumFrameRate(89.1f);
    camera->setMaximumReadoutRate(100.0f);
    camera->setQuantumEfficiency(0.8f);
    camera->setElectronicConversionFactor(0.24f);
    camera->setReadOutNoise(0.7f);
    camera->setDarkCurrentRate(0.06f);

    NBO::DetectorNoiseModel_TypePtr noiseModel = std::make_shared<NBO::DetectorNoiseModel_Type>();
    noiseModel->set("Gaussian");
    camera->setDetectorNoiseModel(noiseModel);

    NBO::CameraIllumination_TypePtr illumination =
        std::make_shared<NBO::CameraIllumination_Type>();
    illumination->set("Back");
    camera->setIllumination(illumination);

    NBO::DigitizerType_TypePtr maxBitDepth = std::make_shared<NBO::DigitizerType_Type>();
    maxBitDepth->set("16bit");
    camera->setMaxBitDepth(maxBitDepth);

    // Detectors are characterized over one or more spectral bands. WavelengthRange is
    // declared inside Detector_Type, so in C++ it is the nested class
    // Detector_Type::InlineWavelengthRange.
    NBO::Detector_Type::InlineWavelengthRangePtr wavelengthRange =
        std::make_shared<NBO::Detector_Type::InlineWavelengthRange>();
    NBO::PositiveFloat_TypePtr cutOn = std::make_shared<NBO::PositiveFloat_Type>();
    cutOn->set(400.0f);
    NBO::PositiveFloat_TypePtr cutOff = std::make_shared<NBO::PositiveFloat_Type>();
    cutOff->set(700.0f);
    wavelengthRange->setCutOn(cutOn);
    wavelengthRange->setCutOff(cutOff);
    wavelengthRange->setPeakWavelength(560.0f);
    wavelengthRange->setWavelengthProfile("orca_fusion_qe.csv");
    camera->addToWavelengthRange_List(wavelengthRange);

    instrument.addToDetectorGroup_List(camera);

    // Step 5: Add the light source
    // LightSource is abstract as well; a Laser is one of its concrete forms.
    NBO::LaserPtr laser = std::make_shared<NBO::Laser>();

    NBO::LightSourceID_TypePtr laserId = std::make_shared<NBO::LightSourceID_Type>();
    laserId->set("LightSource:1");
    laser->setID(laserId);

    laser->setManufacturer("Coherent");
    laser->setModel("OBIS 488");
    laser->setCatalogNumber("1185053");

    laser->setTuneable(false);
    laser->setPulse(false);
    laser->setIsPump(false);
    laser->setIsPumped(false);

    NBO::LaserType_TypePtr laserType = std::make_shared<NBO::LaserType_Type>();
    laserType->set("SemiconductorLaserDiode");
    laser->setType(laserType);

    NBO::LaserModulation_TypePtr modulation = std::make_shared<NBO::LaserModulation_Type>();
    modulation->set("Direct");
    laser->setModulationMechanism(modulation);

    NBO::IlluminationWavelengthRangePtr illuminationRange =
        std::make_shared<NBO::IlluminationWavelengthRange>();
    NBO::PositiveFloat_TypePtr laserCutOn = std::make_shared<NBO::PositiveFloat_Type>();
    laserCutOn->set(486.0f);
    NBO::PositiveFloat_TypePtr laserCutOff = std::make_shared<NBO::PositiveFloat_Type>();
    laserCutOff->set(490.0f);
    illuminationRange->setCutOn(laserCutOn);
    illuminationRange->setCutOff(laserCutOff);
    illuminationRange->setPeakWavelength(488.0f);
    illuminationRange->setIlluminationPower(60.0f);
    laser->addToIlluminationWavelengthRange_List(illuminationRange);

    instrument.addToLightSourceGroup_List(laser);

    // Step 6: Export the description
    std::filesystem::path outputPath = std::filesystem::temp_directory_path() / "instrument.xml";
    instrument.toXmlFile(outputPath.string());
    std::cout << "Instrument description written to " << outputPath.string() << std::endl;

    // Step 7: Read the description back through the model
    std::cout << "Objectives: " << instrument.getObjective_List().size() << std::endl;
    for (const auto &objective : instrument.getObjective_List()) {
        std::cout << "  " << objective->getModel() << " - " << objective->getMagnification()
                  << "x NA " << objective->getLensNA() << std::endl;
    }

    for (const auto &detector : instrument.getDetectorGroup_List()) {
        std::cout << "Detector: " << detector->getManufacturer() << " " << detector->getModel()
                  << std::endl;
    }

    for (const auto &lightSource : instrument.getLightSourceGroup_List()) {
        std::cout << "Light source: " << lightSource->getManufacturer() << " "
                  << lightSource->getModel() << std::endl;
    }

    return 0;
}
// --8<-- [end:main_logic]
