/*
 *  * A plugin for loading proprietary files for Zeiss Xradia X-Ray microscopes 
 *  into ImageJ.
 * @author Michael Sutherland
 * Modified to work with the XRM_files_thumbnails macro 
 * by Brian Metscher, July 2018 - Dec 2025 
 * 
 * To the extent possible under law, Michael Sutherland has waived all
 * copyright and related or neighboring rights to this plugin code.
 * * See the CC0 1.0 Universal license for details:
 *     http://creativecommons.org/publicdomain/zero/1.0/
 *     
 * Additions by Brian Metscher to read 8-bit images, read more image parameters, 
 * and to write parameters to a text file
 * This version of the reader macro is to be called by the IJ macro "XRM files thumbnails." 
 * See https://microtomography.blogspot.com/2024/04/fiji-plugin-to-read-xradia-files.html
 * and https://doi.org/10.5281/zenodo.10580258
*/ 

//package com.nullsquared.imagej;

import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.plugin.PlugIn;
import ij.process.ByteProcessor;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;
import ij.process.ShortProcessor;
import ij.text.TextWindow;
import ij.io.OpenDialog;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.File;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.text.DecimalFormat;

import org.apache.commons.io.FilenameUtils;

import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.poifs.filesystem.DirectoryEntry;
import org.apache.poi.poifs.filesystem.DocumentEntry;
import org.apache.poi.poifs.filesystem.DocumentInputStream;


public class XRM_Thumbnails implements PlugIn {

    // Constants used in Xradia files to denote data types
    private static final int FLOAT_TYPE = 10;
    private static final int INT16_TYPE = 5;    
    private static final int UCHAR_TYPE = 3; 
    
    @Override
    public void run(String args) {
        OpenDialog od = new OpenDialog("Load XRM","","");
        String filename = od.getPath();
        String short_filename = od.getFileName();

		IJ.log("\n"+filename);
        //distinguish projection files from reconstructed slices
        Boolean isTXRM = (filename.endsWith(".txrm"));
        Boolean isXRM = (filename.endsWith(".xrm"));
        Boolean isTXM = (filename.endsWith(".txm"));
        
        IJ.log("Constructing input stream from file...");
        POIFSFileSystem fs;
        try
        {
            InputStream inputStream = new FileInputStream(filename);
            fs = new POIFSFileSystem(inputStream);
        }
        catch (IOException e)
        {
            IJ.log("Error reading XRM file: "+filename);
			return;
        }
        
        // read some parameters from the file 
        DirectoryEntry root = fs.getRoot();            
        int width; 
        int height;
        int numberOfImages;
        int type;
        float pixelSize;
        int binning;
        float OpticalMagnification;
        int voltage;
        int current;
        float expTime = 0; 
        float StoRA = 0;
        float DtoRA = 0;      
        byte[] dateBytes = new byte[24];
        String scanDate = "no data              "; 
        DecimalFormat df2 = new DecimalFormat("0.00");
        DecimalFormat df1 = new DecimalFormat("0.0");
        IJ.log("Reading parameters from "+filename+"...");
        try {
// probably should put each read attempt in a try block
            DirectoryEntry imageInfo = (DirectoryEntry)root.getEntry("ImageInfo");
            //width
            DocumentEntry document = (DocumentEntry)imageInfo.getEntry("ImageWidth");
            DocumentInputStream stream = new DocumentInputStream(document);
            width = stream.readInt();
            stream.close();
            //height
            document = (DocumentEntry)imageInfo.getEntry("ImageHeight");
            stream = new DocumentInputStream(document);
            height = stream.readInt();
            stream.close();
            // data type
            document = (DocumentEntry)imageInfo.getEntry("DataType");
            stream = new DocumentInputStream(document);
            type = stream.readInt();
            stream.close();
            // number of images
            document = (DocumentEntry)imageInfo.getEntry("NoOfImages");
            stream = new DocumentInputStream(document);
            numberOfImages = stream.readInt();
            stream.close();
            // pixel size
            document = (DocumentEntry)imageInfo.getEntry("PixelSize");
            stream = new DocumentInputStream(document);
            pixelSize = Float.intBitsToFloat(stream.readInt()); 
            stream.close();
            // binning 
            document = (DocumentEntry)imageInfo.getEntry("HorizontalBin");
            stream = new DocumentInputStream(document);
            binning = stream.readInt(); 
            stream.close();
            // optical magnification
            document = (DocumentEntry)imageInfo.getEntry("OpticalMagnification");
            stream = new DocumentInputStream(document);
            OpticalMagnification = Float.intBitsToFloat(stream.readInt()); 
            stream.close();
            // voltage
            document = (DocumentEntry)imageInfo.getEntry("Voltage");
            stream = new DocumentInputStream(document);
            voltage = stream.readInt(); 
            stream.close();
            // current
            document = (DocumentEntry)imageInfo.getEntry("Current");
            stream = new DocumentInputStream(document);
            current = stream.readInt(); 
            stream.close();
            
            if (isTXRM)  {	// these params are not in reslices, all 0 in .txm, .xrm files
// prob. should put each read in try block 
				// date and start time
            	document = (DocumentEntry)imageInfo.getEntry("Date");
            	stream = new DocumentInputStream(document);
            	int bytes = stream.read(dateBytes);  // read 1st 24 bytes containing date and time
    	    	scanDate = new String(dateBytes, Charset.forName("UTF-8"));
            	stream.close();
            	// exposure time
            	document = (DocumentEntry)imageInfo.getEntry("ExpTimes");
            	stream = new DocumentInputStream(document);
            	expTime = Float.intBitsToFloat(stream.readInt()); 
            	stream.close();
            	// Source to Rotation Axis distance
            	document = (DocumentEntry)imageInfo.getEntry("StoRADistance");
            	stream = new DocumentInputStream(document);
            	StoRA = Float.intBitsToFloat(stream.readInt()); 
            	if (StoRA > 1000) StoRA = StoRA/1000;
            	stream.close();
            	// Detector to Rotation Axis distance
            	document = (DocumentEntry)imageInfo.getEntry("DtoRADistance");
            	stream = new DocumentInputStream(document);
            	DtoRA = Float.intBitsToFloat(stream.readInt()); 
            	if (DtoRA > 1000) DtoRA = DtoRA/1000;
            	stream.close();
            }
          
            // find bit depth of image to include in output text file
	        int bitDepth = 0; 
	        if (type == FLOAT_TYPE) {bitDepth = 32;}
    	    if (type == INT16_TYPE) {bitDepth = 16;}
        	if (type == UCHAR_TYPE) {bitDepth = 8;}
            
        	 // write to a new text file - Thumbnails version only      
		    try {
	   	    	File metadatafile = new File(filename.replaceFirst("[.][^.]+$", ".txt"));
	   	    	Files.deleteIfExists(metadatafile.toPath()); // PrintWriter might not overwrite
	        	PrintWriter writer = new PrintWriter(metadatafile);	
          		writer.println(filename);
          		if (isTXRM) {  // these params are not in reslices, all 0 in .txm, .xrm files
          			String yrDigit = scanDate.substring(20,21); 
          			if (yrDigit.equals("2")) {writer.println("Scan date: "+scanDate);}
          				// if the date ends in a year, it's likely the old date format
          			else {writer.println("Scan date (m/d/y): "+scanDate.substring(0,17));}
          				// otherwise print as if it's the newer date format 
          		}
          		writer.println("Pixel size (\u00b5m) = "+df2.format(pixelSize));
          		writer.println("Image width = "+Integer.toString(width));
           		writer.println("Image height = "+Integer.toString(height));
           		writer.println("Number of images = "+Integer.toString(numberOfImages));
           		writer.println("Binning = "+Integer.toString(binning));
           		writer.println("Bits per pixel = "+Integer.toString(bitDepth));
           		writer.println("Optical magnification = "+df1.format(OpticalMagnification)+"X");
           		writer.println("Voltage (kV) = "+Integer.toString(voltage));
           		writer.println("Current (\u00b5A) = "+Integer.toString(current));
           		if (isTXRM) {  // these params are not in reslices, all 0 in .txm, .xrm files
          			writer.println("Exposure Time (s) = "+Float.toString(expTime));
           			writer.println("Source to Rotation Axis Distance (mm) = "+df1.format(StoRA));
           			writer.println("Detector to Rotation Axis Distance (mm) = "+df1.format(DtoRA));
           		}
	        	writer.close();
	    	} catch (IOException e) {
	        	e.printStackTrace();
	    	}         
        } catch (IOException e) {
            IJ.log("Couldn't read parameter in XRM file: " + filename + " " + e.toString());
        }  
        
        // Read image data, create and return a stack 
        // NOTE: ImageStack and XRM directory naming are both one-indexed 
        // Loop runs exactly twice for .txrm, whole stack for .txm & .xrm 
        int returnImages = numberOfImages;
        if (isTXRM) returnImages = 2;
        ImageStack stack = new ImageStack(width, height, returnImages);
        IJ.log("Reading image data from "+short_filename+"...");
        
	if (isTXRM) {    // For projections (.txrm) read 1st and middle images for thumbnail        
        for (int imageNumber=1; imageNumber <= returnImages; imageNumber++)
        {
        	int imageIndex = 1;
        	int secondImage = (int)Math.round(numberOfImages/2.0);
        	if (imageNumber==2) imageIndex=secondImage;
            try {
            	// NOTE: ImageData# increments every 100  --why?
                DirectoryEntry imageInfo = (DirectoryEntry)root.getEntry("ImageData"+((int)Math.ceil(imageIndex/100.0)));
                DocumentEntry document = (DocumentEntry)imageInfo.getEntry("Image"+(imageIndex));
                DocumentInputStream stream = new DocumentInputStream(document);
                ImageProcessor proc = null;
                // fill data[] array in reverse order, to turn image right side up
                if (type == FLOAT_TYPE) {
                    // float
                    float[] data = new float[width*height];
                    for (int i = 0; i < width*height; i++) {
                        byte[] bytes = new byte[4]; // space to store float
                        stream.read(bytes);
                        data[width*height - i - 1] = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getFloat();
                    }
                    proc = new FloatProcessor(width, height, data, null); 
                } else if (type == INT16_TYPE) {
                    short[] data = new short[width*height];
                    for (int i = 0; i < width*height; i++) {
                        data[width*height - i - 1] = stream.readShort();
                    }
                    proc = new ShortProcessor(width, height, data, null); 
                } else if (type == UCHAR_TYPE) {
                    byte[] data = new byte[width*height];
                    for (int i = 0; i < width*height; i++) {
                        data[width*height - i - 1] = stream.readByte();
                    }
                    proc = new ByteProcessor(width, height, data, null); 
                } else {
                    IJ.log("Unknown data type in file: " + filename);
                }
                stream.close();
            	stack.setProcessor(proc, imageNumber);
            } catch (IOException e) {
                IJ.log("Couldn't find ImageData1/Image1 in XRM file: " + filename + " " + e.toString());
                return;
            }
        }       	
	}

	else { 		// For sections (.txm or .xrm) read whole image stack 
        for (int imageNumber=1; imageNumber <= returnImages; imageNumber++)
        {
            try {
            	// NOTE: ImageData# increments every 100  --why?
                DirectoryEntry imageInfo = (DirectoryEntry)root.getEntry("ImageData"+((int)Math.ceil(imageNumber/100.0)));
                DocumentEntry document = (DocumentEntry)imageInfo.getEntry("Image"+(imageNumber));
                DocumentInputStream stream = new DocumentInputStream(document);
                ImageProcessor proc = null;
                // fill data[] array in reverse order, to turn image right side up
                if (type == FLOAT_TYPE) {
                    // float
                    float[] data = new float[width*height];
                    for (int i = 0; i < width*height; i++) {
                        byte[] bytes = new byte[4]; // space to store float
                        stream.read(bytes);
                        data[width*height - i - 1] = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getFloat();
                    }
                    proc = new FloatProcessor(width, height, data, null); 
                } else if (type == INT16_TYPE) {
                    short[] data = new short[width*height];
                    for (int i = 0; i < width*height; i++) {
                        data[width*height - i - 1] = stream.readShort();
                    }
                    proc = new ShortProcessor(width, height, data, null); 
                } else if (type == UCHAR_TYPE) {
                    byte[] data = new byte[width*height];
                    for (int i = 0; i < width*height; i++) {
                        data[width*height - i - 1] = stream.readByte();
                    }
                    proc = new ByteProcessor(width, height, data, null); 
                } else {
                    IJ.log("Unknown data type in file: " + filename);
                }
                stream.close();
            	stack.setProcessor(proc, imageNumber);
            } catch (IOException e) {
                IJ.log("Couldn't find ImageData1/Image1 in XRM file: " + filename + " " + e.toString());
                return;
            }
            IJ.showProgress(imageNumber, returnImages);
        }
	}        
        
        ImagePlus imp = new ImagePlus(short_filename, stack);
        imp.show();
    }
}
