package nxm.sys.lib;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;

import nxm.sys.inc.MidasReference;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.inc.Units;

/** A class used to read images. Currently, this only reads the primary (0th) image from the
    image stream/file.

    @author Jeff Schoen
 */
public class ImageFile extends BaseFile implements ImageFileModSwapInterface {

  /** List of available properties in an ImageFile */
  public final String propertyList = BaseFile.propertyList+
    ",XStart,XDelta,XUnits,XFrame,YStart,YDelta,YUnits,YFrame";

  private double      xstart       = 0.0;         // X-axis start
  private double      ystart       = 0.0;         // Y-axis start
  private double      xdelta       = Double.NaN;  // X-axis delta (NaN, means it has not been set yet)
  private double      ydelta       = Double.NaN;  // Y-axis delta (NaN, means it has not been set yet)
  private int         xunits       = Units.NONE;  // X-axis units
  private int         yunits       = Units.NONE;  // Y-axis units
  private int         xframe       = 1;           // X-axis frame-size
  private int         yframe       = 1;           // Y-axis frame-size
  private double      timecode     = 0.0;         // The timecode
  private String      format       = "UNK";       // The image format
  private InputStream inputStream  = null;        // used for iis
  private ImageInputStream iis     = null;        // used by imageReader
  private ImageReader imageReader  = null;        // The reader to use for image input
  private IIOMetadata metadata     = null;        // The image meta-data

  /** ImageFile Default Constructor */
  public ImageFile () { }

  /** ImageFile Constructor
   *  @param ref      Midas Reference
   *  @param filename file name
   *  @deprecated since NeXtMidas 3.5.0 use {@link #ImageFile(MidasReference, Object)}
   */
  @Deprecated
  public ImageFile (Object ref, Object filename) {
    if (ref instanceof MidasReference) {
      init((MidasReference) ref, filename);
    } else {
      init(Convert.ref2Midas(ref), filename);
    }
  }

  /** ImageFile Constructor
   *  @param ref      The reference to use.
   *  @param filename The name of the file to use.
   *  @since NeXtMidas 3.5.0
   */
  public ImageFile (MidasReference ref, Object filename) {
    init(ref,filename);
  }

  public String getMimeType () {
    String fmt = getFormat();

    if (fmt != null) { // check special cases
      fmt = fmt.toLowerCase();
      if (fmt.equals("gif" )) return "image/gif";
      if (fmt.equals("jpg" )) return "image/jpeg";
      if (fmt.equals("jpeg")) return "image/jpeg";
      if (fmt.equals("png" )) return "image/png";
      if (fmt.equals("tif" )) return "image/tiff";
      if (fmt.equals("tiff")) return "image/tiff";
      if (fmt.equals("bmp" )) return "image/bmp";
      if (fmt.equals("wbmp")) return "image/vnd.wap.wbmp";
      if (fmt.equals("pdf" )) return "application/pdf";
    }
    return super.getMimeType(); // unknown
  }

  /** This method is a safety measure that may not be needed
   *  Only if setName is called after the no-arg constructor
   *  and without calling init, then this setName with a
   *  MidasReference must be used to set the FileName flags
   *  according to whether this is Java usage or not.
   *
   *  @param ref      The reference to use.
   *  @param filename The name of the file to use.
   */
  @ProvisionalUseOnly("API may be removed at a later date")
  public void setName (MidasReference ref, Object filename) {
    M = Convert.ref2Midas(ref);
    if (ref!=null) {
      if ((M!=null) && M.isJavaBehavior()) { // NXM-4097 base MAINTAINCASE on Java vs Macro usage
        flags |= MAINTAINCASE;
      }
    }
    setName(filename);
  }

  public void setName (Object filename) {
    super.setName(filename);
    format  = getFileName().getExt().toUpperCase(); // this is a guess, open() will get actual format for input
    comment = (String) getQualifier("COMMENT");
  }

  public boolean open () {
    super.open();
    if (isOpen && isInput) {
      try {
        inputStream = io.getInputStream();
        iis = ImageIO.createImageInputStream(inputStream);
        Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(iis);

        if (!imageReaders.hasNext()) {
          String[] formats = ImageIO.getReaderFormatNames();
          throw new MidasException("No appropriate image reader found for "+getName()+
                      " supported types are "+StringUtil.join(formats)+
                      ". It is also possible the file is corrupt and non-readable.");
        }
        imageReader = imageReaders.next();
        imageReader.setInput(iis, false, false);

        metadata   = imageReader.getImageMetadata(0);
        format     = imageReader.getFormatName();
        int width  = imageReader.getWidth(0);
        int height = imageReader.getHeight(0);

        setXFrame(width);
        if (Double.isNaN(xdelta)) setXDelta(+1.0/width);

        setYFrame(height);
        if (Double.isNaN(ydelta)) setYDelta(-1.0/height);

        io.seek(0); // make sure we seek back to the beginning of file after reading the image metadata
      }
      catch (Exception e) {
        close();
        if ((flags & NOABORT) != 0) return isOpen;
        if (e instanceof MidasException) throw (MidasException)e; // Assume we gave a detailed error message
        throw new MidasException("Unable to read "+getName(),e);
      }
    }
    return isOpen;
  }

  public void close () {
    super.close();
    if (imageReader != null) {
      imageReader.dispose();
      imageReader = null;
    }
    if (iis != null) {
      try { iis.close(); }
      catch (IOException ioe) { M.printStackTrace("ImageFile: Unable to close iis "+getName(), ioe); }
      iis = null;
    }
    if (inputStream != null) {
      try { inputStream.close(); }
      catch (IOException ioe) { M.printStackTrace("ImageFile: Unable to close inputStream "+getName(), ioe); }
      inputStream = null;
    }
    metadata = null;
  }

  public String toString() {
    String str = "Image-"+getFormat()+" ";
    if (io!=null) str += io.getTypeString();
    str += " Resource";
    return str;
  }

  /** Not applicable to image files.
      @param mode n/a
   */
  public void connect (int mode) { }

  /** Not used when reading image.
      @param size n/a
      @return always returns null
   */
  public Data getDataBuffer (int size) { return null; }

  /** Don't use this method for reading images, use {@link #readImage()}.
      @param data n/a
      @param ndo  n/a
      @return Always return -1
   */
  public int read (Data data, int ndo) { return -1; }

  /** This will read the image.
      @return The image in a BufferedImage.
   */
  public BufferedImage readImage () {
    if (!isOpen || !isInput) {
      throw new MidasException("Can not read from "+getName()+" file has not been opened for input.");
    }

    try {
      return imageReader.read(0);
    }
    catch (Exception e) {
      throw new MidasException("Unable to read "+getName(),e);
    }
  }

  /** @deprecated Since NeXtMidas 2.7.1: This method should never have been public. */
  @Deprecated public static void listFormats() {
    String[] formats = ImageIO.getReaderFormatNames();
    Shell.info("ImageIO current format list: "+StringUtil.join(formats));
  }

  public String listHeader() {
    String list =
      "FileName    :  "+getURL()+"\n"
    + "Format      :  "+getFormat()+"\n"
    + "Size        :  "+getSize()+"\n"
    + "X units     :  "+DataFile.getUnitsName(getXUnits())+"\n"
    + "X start     :  "+getXStart()+"\n"
    + "X delta     :  "+getXDelta()+"\n"
    + "X frame     :  "+getXFrame()+"\n"
    + "Y units     :  "+DataFile.getUnitsName(getYUnits())+"\n"
    + "Y start     :  "+getYStart()+"\n"
    + "Y delta     :  "+getYDelta()+"\n"
    + "Y frame     :  "+getYFrame()+"\n"
    + "Comment     :  "+getComment()+"\n";
    return list;
  }

  /** Gets any meta-data information associated with this image. For more details, please see
      {@link ImageReader#getImageMetadata} and {@link IIOMetadata}.
      @return The meta-data.
      @since NeXtMidas 2.7.1
   */
  public IIOMetadata getImageMetadata () {
    return metadata;
  }

  public int    getType ()   { return 7000;   } // JC4: Why does this return 7000?
  public int    getMode ()   { return 1;      }
  public String getFormat () { return format; } // JC4: This doesn't match PlotFile
  public double getStart ()  { return ystart; }
  public double getDelta ()  { return ydelta; }
  public int    getUnits ()  { return yunits; }
  public double getXStart () { return xstart; }
  public double getXDelta () { return xdelta; }
  public int    getXUnits () { return xunits; }
  public int    getXFrame () { return xframe; }
  public double getYStart () { return ystart; }
  public double getYDelta () { return ydelta; }
  public int    getYUnits () { return yunits; }
  public int    getYFrame () { return yframe; }

  public double getTimeAt (double offset) { return timecode; }
  /** Sets the image format
      @param value new image format
   */
  public void setFormat (String value) { format = value; }
  /** Sets the X-axis start value
      @param value new X-axis start value
   */
  public void setXStart (double value) { xstart = value; }
  /** Sets the X-axis delta
      @param value new X-axis delta
   */
  public void setXDelta (double value) { xdelta = value; }
  /** Sets the X-axis units
      @param value new X-axis units
   */
  public void setXUnits (int value) { xunits = value; }
  /** Sets the X-axis frame size
      @param value new X-axis frame size
   */
  public void setXFrame (int value) { xframe = value; }
  /** Sets the Y-axis start value
      @param value new Y-axis start value
   */
  public void setYStart (double value) { ystart = value; }
  /** Sets the Y-axis delta
      @param value new Y-axis delta
   */
  public void setYDelta (double value) { ydelta = value; }
  /** Sets the Y-axis units
      @param value new Y-axis units
   */
  public void setYUnits (int value) { yunits = value; }
  /** Sets the Y-axis frame size
      @param value new Y-axis frame size
   */
  public void setYFrame (int value) { yframe = value; }
  /** Sets the time code
      @param value new timecode
   */
  public void setTimeCode (double value) { timecode = value; }
}
