/*
 * 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: inscoper-nbo-<version>.jar must be on the classpath.
 */

import com.inscoper.nbo.Channel;
import com.inscoper.nbo.ChannelContrastMethod_Type;
import com.inscoper.nbo.ChannelID_Type;
import com.inscoper.nbo.Denomination_Type;
import com.inscoper.nbo.Image;
import com.inscoper.nbo.ImageID_Type;
import com.inscoper.nbo.ImagingMethodList_Type;
import com.inscoper.nbo.LightPath;
import com.inscoper.nbo.LSID_Type;
import com.inscoper.nbo.MetadataOnly;
import com.inscoper.nbo.MetadataOnlyVector;
import com.inscoper.nbo.NonNegativeInt_Type;
import com.inscoper.nbo.PixelType_Type;
import com.inscoper.nbo.Pixels;
import com.inscoper.nbo.Pixels_Choice;
import com.inscoper.nbo.PixelsDimensionOrder_Type;
import com.inscoper.nbo.PixelsID_Type;
import com.inscoper.nbo.Plane;
import com.inscoper.nbo.PlaneID_Type;
import com.inscoper.nbo.PositiveFloat_Type;
import com.inscoper.nbo.PositiveInt_Type;
import com.inscoper.nbo.StageLabel;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Consumer;

public class ImageMetadata {

    // --8<-- [start:main_logic]
    // Values that would come from the acquisition software
    static final int SIZE_X = 2304;
    static final int SIZE_Y = 2304;
    static final int SIZE_Z = 5;
    static final int SIZE_C = 2;
    static final int SIZE_T = 3;
    static final float PIXEL_SIZE_XY = 0.108f;   // micrometers
    static final float Z_STEP = 0.5f;            // micrometers

    record ChannelSpec(String name, String imagingMethod, String contrast) {
    }

    public static void main(String[] args) {
        // Step 1: Describe the pixel block
        Pixels pixels = new Pixels();

        PixelsID_Type pixelsId = new PixelsID_Type();
        pixelsId.set("Pixels:1");
        pixels.setID(pixelsId);

        PixelsDimensionOrder_Type dimensionOrder = new PixelsDimensionOrder_Type();
        dimensionOrder.set("XYZTC");
        pixels.setDimensionOrder(dimensionOrder);

        setSize(pixels::setSizeX, SIZE_X);
        setSize(pixels::setSizeY, SIZE_Y);
        setSize(pixels::setSizeZ, SIZE_Z);
        setSize(pixels::setSizeC, SIZE_C);
        setSize(pixels::setSizeT, SIZE_T);

        PixelType_Type pixelType = new PixelType_Type();
        pixelType.set("uint16");
        pixels.setPixelType(pixelType);

        // Step 2: Add the physical calibration
        setPhysicalSize(pixels::setPhysicalSizeX, PIXEL_SIZE_XY);
        setPhysicalSize(pixels::setPhysicalSizeY, PIXEL_SIZE_XY);
        setPhysicalSize(pixels::setPhysicalSizeZ, Z_STEP);

        // 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.
        Pixels_Choice pixelsChoice = new Pixels_Choice();
        MetadataOnlyVector metadataOnlyList = new MetadataOnlyVector();
        metadataOnlyList.add(new MetadataOnly());
        pixelsChoice.addMetadataOnly(metadataOnlyList);
        pixels.setpixels_choice(pixelsChoice);

        // Step 4: Describe the channels
        ChannelSpec[] channels = {
            new ChannelSpec("DAPI", "Wide-field_Fluorescence", "Fluorescence"),
            new ChannelSpec("Brightfield", "Transmitted_Bright-field", "Brightfield"),
        };

        int channelIndex = 0;
        for (ChannelSpec spec : channels) {
            Channel channel = new Channel();

            ChannelID_Type channelId = new ChannelID_Type();
            channelId.set("Channel:" + channelIndex);
            channel.setID(channelId);

            Denomination_Type name = new Denomination_Type();
            name.set(spec.name());
            channel.setName(name);

            ImagingMethodList_Type illuminationType = new ImagingMethodList_Type();
            illuminationType.set(spec.imagingMethod());
            channel.setIlluminationType(illuminationType);

            ChannelContrastMethod_Type contrastMethod = new ChannelContrastMethod_Type();
            contrastMethod.set(spec.contrast());
            channel.setContrastMethod(contrastMethod);

            // The light path records the optics the channel was acquired through
            LightPath lightPath = new LightPath();
            LSID_Type lightPathId = new LSID_Type();
            lightPathId.set("LightPath:" + 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++) {
                    Plane plane = new Plane();

                    PlaneID_Type planeId = new PlaneID_Type();
                    planeId.set("Plane:" + theT + ":" + theC + ":" + theZ);
                    plane.setID(planeId);

                    setIndex(plane::setTheZ, theZ);
                    setIndex(plane::setTheC, theC);
                    setIndex(plane::setTheT, theT);

                    plane.setTimestamp(timestamp);
                    timestamp += 0.05f;

                    pixels.addToPlane_List(plane);
                }
            }
        }

        // Step 6: Attach the pixel block to an image and export
        Image image = new Image();

        ImageID_Type imageId = new ImageID_Type();
        imageId.set("Image:1");
        image.setID(imageId);

        Denomination_Type imageName = new Denomination_Type();
        imageName.set("Well A1 - Field 1");
        image.setName(imageName);

        image.setPixels(pixels);

        StageLabel stageLabel = new StageLabel();
        stageLabel.setName("Well A1 - Field 1");
        stageLabel.setX(12500.0f);
        stageLabel.setY(8300.0f);
        stageLabel.setZ(4210.5f);
        image.addToStageLabel_List(stageLabel);

        Path outputPath = Paths.get(System.getProperty("java.io.tmpdir"), "image_metadata.xml");
        image.toXmlFile(outputPath.toString());
        System.out.println("Image description written to " + outputPath);
        System.out.println("Planes: " + image.getPixels().getPlane_List().size());
        System.out.print("Channels: ");
        for (Channel channel : image.getPixels().getChannel_List()) {
            System.out.print(channel.getName().get() + " ");
        }
        System.out.println();
    }

    // Dimension sizes, physical sizes, and plane indices are restricted types:
    // instantiate, set, and pass the object to the accessor.
    static void setSize(Consumer<PositiveInt_Type> setter, int value) {
        PositiveInt_Type size = new PositiveInt_Type();
        size.set(value);
        setter.accept(size);
    }

    static void setPhysicalSize(Consumer<PositiveFloat_Type> setter, float value) {
        PositiveFloat_Type physicalSize = new PositiveFloat_Type();
        physicalSize.set(value);
        setter.accept(physicalSize);
    }

    static void setIndex(Consumer<NonNegativeInt_Type> setter, int value) {
        NonNegativeInt_Type indexValue = new NonNegativeInt_Type();
        indexValue.set(value);
        setter.accept(indexValue);
    }
    // --8<-- [end:main_logic]
}
