package ReadEDF;

import ij.CompositeImage;
import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.LookUpTable;
import ij.Macro;
import ij.Prefs;
import ij.WindowManager;
import ij.gui.GenericDialog;
import ij.gui.ProgressBar;
import ij.io.FileInfo;
import ij.io.ImageReader;
import ij.io.Opener;
import ij.io.RandomAccessStream;
import ij.measure.Calibration;
import ij.process.ByteProcessor;
import ij.process.ColorProcessor;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;
import ij.process.ShortProcessor;

import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.MemoryImageSource;
import java.io.ByteArrayInputStream;
import java.io.CharArrayWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Properties;
import java.util.zip.GZIPInputStream;


/**
 * Opens or reverts an image specified by a FileInfo object. Images can
 * be loaded from either a file (directory+fileName) or a URL (url+fileName).
 * Here is an example:  
 * <pre>
 *   public class FileInfo_Test implements PlugIn {
 *     public void run(String arg) {
 *       FileInfo fi = new FileInfo();
 *       fi.width = 256;
 *       fi.height = 254;
 *       fi.offset = 768;
 *       fi.fileName = "blobs.tif";
 *       fi.directory = "/Users/wayne/Desktop/";
 *       new FileOpener(fi).open();
 *     }  
 *   }  
 * </pre> 
 */
public class FileOpener {

    private FileInfo fi;
    private int width, height;
    private static boolean showConflictMessage = true;
    private double minValue, maxValue;
    private ImageProcessor ipBack;

    public FileOpener(FileInfo fi) {
        this.fi = fi;
        if (fi!=null) {
            width = fi.width;
            height = fi.height;
        }
        if (IJ.debugMode) IJ.log("FileOpener: "+fi);
    }
    
    /** Opens the image and displays it. */
    public void open() {
        open(true);
    }
    
    /** Opens the image. Displays it if 'show' is
    true. Returns an ImagePlus object if successful. */
    public ImagePlus open(boolean show) {
        ImagePlus imp=null;
        Object pixels;
        ProgressBar pb=null;
        ImageProcessor ip;
        
        ColorModel cm = createColorModel(fi);
        if (fi.nImages>1)
            {return openStack(cm, show);}
        switch (fi.fileType) {
            case FileInfo.GRAY8:
            case FileInfo.COLOR8:
            case FileInfo.BITMAP:
                pixels = readPixels(fi);
                if (pixels==null) return null;
                ip = new ByteProcessor(width, height, (byte[])pixels, cm);
                imp = new ImagePlus(fi.fileName, ip);
                break;
            case FileInfo.GRAY16_SIGNED:
            case FileInfo.GRAY16_UNSIGNED:
            case FileInfo.GRAY12_UNSIGNED:
                pixels = readPixels(fi);
                if (pixels==null) return null;
                ip = new ShortProcessor(width, height, (short[])pixels, cm);
                imp = new ImagePlus(fi.fileName, ip);
                break;
            case FileInfo.GRAY32_INT:
            case FileInfo.GRAY32_UNSIGNED:
            case FileInfo.GRAY32_FLOAT:
            case FileInfo.GRAY24_UNSIGNED:
            case FileInfo.GRAY64_FLOAT:
                pixels = readPixels(fi);
                if (pixels==null) return null;
                ip = new FloatProcessor(width, height, (float[])pixels, cm);
                imp = new ImagePlus(fi.fileName, ip);
                break;
            case FileInfo.RGB:
            case FileInfo.BGR:
            case FileInfo.ARGB:
            case FileInfo.BARG:
            case FileInfo.RGB_PLANAR:
                pixels = readPixels(fi);
                if (pixels==null) return null;
                ip = new ColorProcessor(width, height, (int[])pixels);
                imp = new ImagePlus(fi.fileName, ip);
                break;
            case FileInfo.RGB48:
            case FileInfo.RGB48_PLANAR:
                boolean planar = fi.fileType==FileInfo.RGB48_PLANAR;
                Object[] pixelArray = (Object[])readPixels(fi);
                if (pixelArray==null) return null;
                ImageStack stack = new ImageStack(width, height);
                stack.addSlice("Red", pixelArray[0]);
                stack.addSlice("Green", pixelArray[1]);
                stack.addSlice("Blue", pixelArray[2]);
                imp = new ImagePlus(fi.fileName, stack);
                imp.setDimensions(3, 1, 1);
                if (planar)
                    imp.getProcessor().resetMinAndMax();
                imp.setFileInfo(fi);
                int mode = CompositeImage.COMPOSITE;
                if (fi.description!=null) {
                    if (fi.description.indexOf("mode=color")!=-1)
                    mode = CompositeImage.COLOR;
                    else if (fi.description.indexOf("mode=gray")!=-1)
                    mode = CompositeImage.GRAYSCALE;
                }
                imp = new CompositeImage(imp, mode);
                if (!planar && fi.displayRanges==null) {
                    for (int c=1; c<=3; c++) {
                        imp.setPosition(c, 1, 1);
                        imp.setDisplayRange(minValue, maxValue);
                    }
                    imp.setPosition(1, 1, 1);
                }
                break;
        }
        imp.setFileInfo(fi);
        setCalibration(imp);
        if (fi.info!=null)
            imp.setProperty("Info", fi.info);
        if (fi.sliceLabels!=null&&fi.sliceLabels.length==1&&fi.sliceLabels[0]!=null)
            imp.setProperty("Label", fi.sliceLabels[0]);
//        if (show) imp.show();
        if (show) ipBack = imp.getProcessor();
        return imp;
    }

    /** Opens a stack of images. */
    ImagePlus openStack(ColorModel cm, boolean show) {
        ImageStack stack = new ImageStack(fi.width, fi.height, cm);
        long skip = fi.longOffset>0?fi.longOffset:fi.offset;
        Object pixels;
        try {
            ImageReader reader = new ImageReader(fi);
            InputStream is = createInputStream(fi);
            if (is==null) return null;
            IJ.resetEscape();
            for (int i=1; i<=fi.nImages; i++) {
                IJ.showStatus("Reading: " + i + "/" + fi.nImages);
                if (IJ.escapePressed()) {
                    IJ.beep();
                    IJ.showProgress(1.0);
                    return null;
                }
                pixels = reader.readPixels(is, skip);
                if (pixels==null) break;
                stack.addSlice(null, pixels);
                skip = fi.gapBetweenImages;
                IJ.showProgress(i, fi.nImages);
            }
            is.close();
        }
        catch (Exception e) {
            IJ.log("" + e);
        }
        catch(OutOfMemoryError e) {
            IJ.outOfMemory(fi.fileName);
            stack.trim();
        }
        IJ.showProgress(1.0);
        if (stack.getSize()==0)
            return null;
        if (fi.sliceLabels!=null && fi.sliceLabels.length<=stack.getSize()) {
            for (int i=0; i<fi.sliceLabels.length; i++)
                stack.setSliceLabel(fi.sliceLabels[i], i+1);
        }
        ImagePlus imp = new ImagePlus(fi.fileName, stack);
        if (fi.info!=null)
            imp.setProperty("Info", fi.info);
        if (show) imp.show();
        imp.setFileInfo(fi);
        setCalibration(imp);
        ImageProcessor ip = imp.getProcessor();
        if (ip.getMin()==ip.getMax())  // find stack min and max if first slice is blank
            setStackDisplayRange(imp);
        IJ.showProgress(1.0);
        return imp;
    }

    void setStackDisplayRange(ImagePlus imp) {
        ImageStack stack = imp.getStack();
        double min = Double.MAX_VALUE;
        double max = -Double.MAX_VALUE;
        int n = stack.getSize();
        for (int i=1; i<=n; i++) {
            IJ.showStatus("Calculating stack min and max: "+i+"/"+n);
            ImageProcessor ip = stack.getProcessor(i);
            ip.resetMinAndMax();
            if (ip.getMin()<min)
                min = ip.getMin();
            if (ip.getMax()>max)
                max = ip.getMax();
        }
        imp.getProcessor().setMinAndMax(min, max);
        imp.updateAndDraw();
    }
    
    /** Restores original disk or network version of image. */
    public void revertToSaved(ImagePlus imp) {
        Image img;
        ProgressBar pb = IJ.getInstance().getProgressBar();
        ImageProcessor ip;
        String path = fi.directory + fi.fileName;
        
        if (fi.fileFormat==fi.GIF_OR_JPG) {
            // restore gif or jpg
            img = Toolkit.getDefaultToolkit().createImage(path);
            imp.setImage(img);
            if (imp.getType()==ImagePlus.COLOR_RGB)
                Opener.convertGrayJpegTo8Bits(imp);
            return;
        }
                
        if (fi.fileFormat==fi.DICOM) {
            // restore DICOM
            ImagePlus imp2 = (ImagePlus)IJ.runPlugIn("ij.plugin.DICOM", path);
            if (imp2!=null)
                imp.setProcessor(null, imp2.getProcessor());
            return;
        }

        if (fi.fileFormat==fi.BMP) {
            // restore BMP
            ImagePlus imp2 = (ImagePlus)IJ.runPlugIn("ij.plugin.BMP_Reader", path);
            if (imp2!=null)
                imp.setProcessor(null, imp2.getProcessor());
            return;
        }

        if (fi.fileFormat==fi.PGM) {
            // restore PGM
            ImagePlus imp2 = (ImagePlus)IJ.runPlugIn("ij.plugin.PGM_Reader", path);
            if (imp2!=null)
                imp.setProcessor(null, imp2.getProcessor());
            return;
        }

        if (fi.fileFormat==fi.ZIP_ARCHIVE) {
            // restore ".zip" file
            ImagePlus imp2 = (new Opener()).openZip(path);
            if (imp2!=null)
                imp.setProcessor(null, imp2.getProcessor());
            return;
        }

        // restore PNG or another image opened using ImageIO
        if (fi.fileFormat==fi.IMAGEIO) {
//            ImagePlus imp2 = (new Opener()).openUsingImageIO(path);
//            if (imp2!=null) imp.setProcessor(null, imp2.getProcessor());
            return;
        }

        if (fi.nImages>1)
            return;
        
        ColorModel cm;
        if (fi.url==null || fi.url.equals(""))
            IJ.showStatus("Loading: " + path);
        else
            IJ.showStatus("Loading: " + fi.url + fi.fileName);
        Object pixels = readPixels(fi);
        if (pixels==null) return;
        cm = createColorModel(fi);
        switch (fi.fileType) {
            case FileInfo.GRAY8:
            case FileInfo.COLOR8:
            case FileInfo.BITMAP:
                ip = new ByteProcessor(width, height, (byte[])pixels, cm);
                imp.setProcessor(null, ip);
                break;
            case FileInfo.GRAY16_SIGNED:
            case FileInfo.GRAY16_UNSIGNED:
            case FileInfo.GRAY12_UNSIGNED:
                ip = new ShortProcessor(width, height, (short[])pixels, cm);
                imp.setProcessor(null, ip);
                break;
            case FileInfo.GRAY32_INT:
            case FileInfo.GRAY32_FLOAT:
                ip = new FloatProcessor(width, height, (float[])pixels, cm);
                imp.setProcessor(null, ip);
                break;
            case FileInfo.RGB:
            case FileInfo.BGR:
            case FileInfo.ARGB:
            case FileInfo.RGB_PLANAR:
                img = Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(width, height, (int[])pixels, 0, width));
                imp.setImage(img);
                break;
        }
    }
    
    void setCalibration(ImagePlus imp) {
        if (fi.fileType==FileInfo.GRAY16_SIGNED) {
            if (IJ.debugMode) IJ.log("16-bit signed");
            double[] coeff = new double[2];
            coeff[0] = -32768.0;
            coeff[1] = 1.0;
            imp.getLocalCalibration().setFunction(Calibration.STRAIGHT_LINE, coeff, "gray value");
        }
        
        Properties props = decodeDescriptionString(fi);
        Calibration cal = imp.getCalibration();
        boolean calibrated = false;
        if (fi.pixelWidth>0.0 && fi.unit!=null) {
            cal.pixelWidth = fi.pixelWidth;
            cal.pixelHeight = fi.pixelHeight;
            cal.pixelDepth = fi.pixelDepth;
            cal.setUnit(fi.unit);
            calibrated = true;
        }
        
        if (fi.valueUnit!=null) {
            int f = fi.calibrationFunction;
            if ((f>=Calibration.STRAIGHT_LINE && f<=Calibration.RODBARD2 && fi.coefficients!=null)
            || f==Calibration.UNCALIBRATED_OD) {
                boolean zeroClip = props!=null && props.getProperty("zeroclip", "false").equals("true");    
                cal.setFunction(f, fi.coefficients, fi.valueUnit, zeroClip);
                calibrated = true;
            }
        }
        
        if (calibrated)
            checkForCalibrationConflict(imp, cal);
        
        if (fi.frameInterval!=0.0)
            cal.frameInterval = fi.frameInterval;
        
        if (props==null)
            return;
                    
        cal.xOrigin = getDouble(props,"xorigin");
        cal.yOrigin = getDouble(props,"yorigin");
        cal.zOrigin = getDouble(props,"zorigin");
        cal.info = props.getProperty("info");       
                
        cal.fps = getDouble(props,"fps");
        cal.loop = getBoolean(props, "loop");
        cal.frameInterval = getDouble(props,"finterval");
        cal.setTimeUnit(props.getProperty("tunit", "sec"));

        double displayMin = getDouble(props,"min");
        double displayMax = getDouble(props,"max");
        if (!(displayMin==0.0&&displayMax==0.0)) {
            int type = imp.getType();
            ImageProcessor ip = imp.getProcessor();
            if (type==ImagePlus.GRAY8 || type==ImagePlus.COLOR_256)
                ip.setMinAndMax(displayMin, displayMax);
            else if (type==ImagePlus.GRAY16 || type==ImagePlus.GRAY32) {
                if (ip.getMin()!=displayMin || ip.getMax()!=displayMax)
                    ip.setMinAndMax(displayMin, displayMax);
            }
        }
        
        int stackSize = imp.getStackSize();
        if (stackSize>1) {
            int channels = (int)getDouble(props,"channels");
            int slices = (int)getDouble(props,"slices");
            int frames = (int)getDouble(props,"frames");
            if (channels==0) channels = 1;
            if (slices==0) slices = 1;
            if (frames==0) frames = 1;
            //IJ.log("setCalibration: "+channels+"  "+slices+"  "+frames);
            if (channels*slices*frames==stackSize) {
                imp.setDimensions(channels, slices, frames);
                if (getBoolean(props, "hyperstack"))
                    imp.setOpenAsHyperStack(true);
            }
        }
    }

        
    void checkForCalibrationConflict(ImagePlus imp, Calibration cal) {
        Calibration gcal = imp.getGlobalCalibration();
        if  (gcal==null || !showConflictMessage || IJ.isMacro())
            return;
        if (cal.pixelWidth==gcal.pixelWidth && cal.getUnit().equals(gcal.getUnit()))
            return;
        GenericDialog gd = new GenericDialog(imp.getTitle());
        gd.addMessage("The calibration of this image conflicts\nwith the current global calibration.");
        gd.addCheckbox("Disable_Global Calibration", true);
        gd.addCheckbox("Disable_these Messages", false);
        gd.showDialog();
        if (gd.wasCanceled()) return;
        boolean disable = gd.getNextBoolean();
        if (disable) {
            imp.setGlobalCalibration(null);
            imp.setCalibration(cal);
            WindowManager.repaintImageWindows();
        }
        boolean dontShow = gd.getNextBoolean();
        if (dontShow) showConflictMessage = false;
    }

    /** Returns an IndexColorModel for the image specified by this FileInfo. */
    public ColorModel createColorModel(FileInfo fi) {
        if (fi.fileType==FileInfo.COLOR8 && fi.lutSize>0)
            return new IndexColorModel(8, fi.lutSize, fi.reds, fi.greens, fi.blues);
        else
            return LookUpTable.createGrayscaleColorModel(fi.whiteIsZero);
    }

    /** Returns an InputStream for the image described by this FileInfo. */
    public InputStream createInputStream(FileInfo fi) throws IOException, MalformedURLException {
        InputStream is = null;
        boolean gzip = fi.fileName!=null && (fi.fileName.endsWith(".gz")||fi.fileName.endsWith(".GZ"));
        if (fi.inputStream!=null)
            is = fi.inputStream;
        else if (fi.url!=null && !fi.url.equals(""))
            is = new URL(fi.url+fi.fileName).openStream();
        else {
            if (fi.directory.length()>0 && !fi.directory.endsWith(Prefs.separator))
                fi.directory += Prefs.separator;
            File f = new File(fi.directory + fi.fileName);
            if (gzip) fi.compression = FileInfo.COMPRESSION_UNKNOWN;
            if (f==null || f.isDirectory() || !validateFileInfo(f, fi))
                is = null;
            else
                is = new FileInputStream(f);
        }
        if (is!=null) {
            if (fi.compression>=FileInfo.LZW)
                is = new RandomAccessStream(is);
            else if (gzip)
                is = new GZIPInputStream(is);
        }
        return is;
    }
    
    static boolean validateFileInfo(File f, FileInfo fi) {
        long offset = fi.longOffset>0?fi.longOffset:fi.offset;
        long length = 0;
        if (fi.width<=0 || fi.height<0) {
           error("Width or height <= 0.", fi, offset, length);
           return false;
        }
        if (offset>=0 && offset<1000)
             return true;
        if (offset<0) {
           error("Offset is negative.", fi, offset, length);
           return false;
        }
        if (fi.fileType==FileInfo.BITMAP || fi.compression!=FileInfo.COMPRESSION_NONE)
            return true;
        length = f.length();
        long size = fi.width*fi.height*fi.getBytesPerPixel();
        size = fi.nImages>1?size:size/4;
        if (fi.height==1) size = 0; // allows plugins to read info of unknown length at end of file
        if (offset+size>length) {
           error("Offset + image size > file length.", fi, offset, length);
           return false;
        }
        return true;
    }

    static void error(String msg, FileInfo fi, long offset, long length) {
        IJ.error("FileOpener", "FileInfo parameter error. \n"
            +msg + "\n \n"
            +"  Width: " + fi.width + "\n"
            +"  Height: " + fi.height + "\n"
            +"  Offset: " + offset + "\n"
            +"  Bytes/pixel: " + fi.getBytesPerPixel() + "\n"
            +(length>0?"  File length: " + length + "\n":"")
        );
    }


    /** Reads the pixel data from an image described by a FileInfo object. */
    Object readPixels(FileInfo fi) {
        Object pixels = null;
        try {
            InputStream is = createInputStream(fi);
            if (is==null)
                return null;
            ImageReader reader = new ImageReader(fi);
            pixels = reader.readPixels(is);
            minValue = reader.min;
            maxValue = reader.max;
            is.close();
        }
        catch (Exception e) {
            if (!Macro.MACRO_CANCELED.equals(e.getMessage())) {
                CharArrayWriter caw = new CharArrayWriter();
                PrintWriter pw = new PrintWriter(caw);
                e.printStackTrace(pw);
                String s = caw.toString();
                if (IJ.getInstance()!=null)
                    new ij.text.TextWindow("Exception", s, 350, 250);
                else
                    IJ.log(s);
            }
        }
        catch(OutOfMemoryError oome){
        	throw new java.lang.OutOfMemoryError("Heap space out of memory");
        }
        return pixels;
    }

    public Properties decodeDescriptionString(FileInfo fi) {
        if (fi.description==null || fi.description.length()<7)
            return null;
        if (IJ.debugMode)
            IJ.log("Image Description: " + new String(fi.description).replace('\n',' '));
        if (!fi.description.startsWith("ImageJ"))
            return null;
        Properties props = new Properties();
        InputStream is = new ByteArrayInputStream(fi.description.getBytes());
        try {props.load(is); is.close();}
        catch (IOException e) {return null;}
        fi.unit = props.getProperty("unit","");
        Double n = getNumber(props,"cf");
        if (n!=null) fi.calibrationFunction = n.intValue();
        double c[] = new double[5];
        int count = 0;
        for (int i=0; i<5; i++) {
            n = getNumber(props,"c"+i);
            if (n==null) break;
            c[i] = n.doubleValue();
            count++;
        }
        if (count>=2) {
            fi.coefficients = new double[count];
            for (int i=0; i<count; i++)
                fi.coefficients[i] = c[i];          
        }
        fi.valueUnit = props.getProperty("vunit");
        n = getNumber(props,"images");
        if (n!=null && n.doubleValue()>1.0)
            fi.nImages = (int)n.doubleValue();
        if (fi.nImages>1) {
            double spacing = getDouble(props,"spacing");
            if (spacing!=0.0)
                fi.pixelDepth = spacing;
        }
        return props;
    }

    private Double getNumber(Properties props, String key) {
        String s = props.getProperty(key);
        if (s!=null) {
            try {
                return Double.valueOf(s);
            } catch (NumberFormatException e) {}
        }   
        return null;
    }
    
    private double getDouble(Properties props, String key) {
        Double n = getNumber(props, key);
        return n!=null?n.doubleValue():0.0;
    }
    
    private boolean getBoolean(Properties props, String key) {
        String s = props.getProperty(key);
        return s!=null&&s.equals("true")?true:false;
    }
    
    public static void setShowConflictMessage(boolean b) {
        showConflictMessage = b;
    }

    ImageProcessor getIp(){
    	return ipBack;
    }
}