/*
 * #%L
 * OME Bio-Formats package for reading and converting biological file formats.
 * %%
 * Copyright (C) 2005 - 2017 Open Microscopy Environment:
 *   - Board of Regents of the University of Wisconsin-Madison
 *   - Glencoe Software, Inc.
 *   - University of Dundee
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-2.0.html>.
 * #L%
 */

package loci.formats.in;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import loci.common.DateTools;
import loci.common.Location;
import loci.common.RandomAccessInputStream;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.FilePattern;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.meta.MetadataStore;
import loci.formats.tiff.IFD;
import loci.formats.tiff.TiffParser;

import ome.xml.model.enums.NamingConvention;
import ome.xml.model.primitives.NonNegativeInteger;
import ome.xml.model.primitives.PositiveInteger;
import ome.xml.model.primitives.Timestamp;

import ome.units.quantity.Length;
import ome.units.quantity.Temperature;
import ome.units.quantity.Time;
import ome.units.UNITS;

import org.apache.commons.lang.ArrayUtils;

/**
 * MetamorphTiffReader is the file format reader for TIFF files produced by
 * Metamorph software version 7.5 and above.
 *
 * @author Melissa Linkert melissa at glencoesoftware.com
 * @author Thomas Caswell tcaswell at uchicago.edu
 */
public class MetamorphTiffReader extends BaseTiffReader {

  // -- Constants --

  private static final String DATE_FORMAT = "yyyyMMdd HH:mm:ss";

  // -- Fields --

  private String[] files;
  private int wellCount = 1;
  private int fieldRowCount = 1;
  private int fieldColumnCount = 1;
  private boolean dualCamera = false;

  // -- Constructor --

  /** Constructs a new Metamorph TIFF reader. */
  public MetamorphTiffReader() {
    super("Metamorph TIFF", new String[] {"tif", "tiff"});
    suffixSufficient = false;
    domains = new String[] {FormatTools.LM_DOMAIN, FormatTools.HCS_DOMAIN};
    datasetDescription = "One or more .tif/.tiff files";
  }

  // -- IFormatReader API methods --

  /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
  @Override
  public boolean isThisType(RandomAccessInputStream stream) throws IOException {
    TiffParser tp = new TiffParser(stream);
    String comment = tp.getComment();
    if (comment == null) return false;
    comment = comment.trim();
    return comment.startsWith("<MetaData>") && comment.endsWith("</MetaData>");
  }

  /* @see loci.formats.IFormatReader#getDomains() */
  @Override
  public String[] getDomains() {
    FormatTools.assertId(currentId, true, 1);
    String[] domain = new String[1];
    domain[0] =
      files.length == 1 ? FormatTools.LM_DOMAIN : FormatTools.HCS_DOMAIN;
    return domain;
  }

  /* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */
  @Override
  public String[] getUsedFiles(boolean noPixels) {
    FormatTools.assertId(currentId, true, 1);
    return noPixels ? ArrayUtils.EMPTY_STRING_ARRAY : files;
  }

  /* @see loci.formats.IFormatReader#close(boolean) */
  @Override
  public void close(boolean fileOnly) throws IOException {
    super.close(fileOnly);
    if (!fileOnly) {
      files = null;
      wellCount = 1;
      fieldRowCount = 1;
      fieldColumnCount = 1;
      dualCamera = false;
    }
  }

  /**
   * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int)
   */
  @Override
  public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h)
    throws FormatException, IOException
  {
    if (getSeriesCount() == 1 && files.length == 1 && !dualCamera) {
      return super.openBytes(no, buf, x, y, w, h);
    }

    int[] lengths = new int[] {getSizeZ(), getEffectiveSizeC(),
      fieldColumnCount, fieldRowCount, wellCount, getSizeT()};

    int[] zct = getZCTCoords(no);
    Well well = getWell(getSeries());
    int[] position = new int[] {zct[0], zct[1], well.fieldCol,
      well.fieldRow, well.well, zct[2]};

    if (dualCamera) {
      position[1] = 0;
    }

    int fileIndex = FormatTools.positionToRaster(lengths, position);
    final RandomAccessInputStream s;
    if (fileIndex < files.length) {
      s = new RandomAccessInputStream(files[fileIndex]);
    }
    else {
      s = new RandomAccessInputStream(files[0]);
    }
    try (RandomAccessInputStream stream = s) {
      TiffParser parser = new TiffParser(s);
      int ndx = getSeries() * getSizeZ() * getSizeT() + no;
      int[] pos = getZCTCoords(no);
      if (dualCamera) {
        int channel = pos[1];
        pos[1] = 0;
        ndx = getSeries() * getSizeZ() * getSizeT() +
          FormatTools.positionToRaster(new int[] {getSizeZ(), 1, getSizeT()}, pos);
        pos[1] = channel;
      }
      IFD ifd = files.length == 1 ? ifds.get(ndx) : parser.getFirstIFD();
      int realX = dualCamera ? (pos[1] == 0 ? x : x + pos[1] * getSizeX()) : x;
      parser.getSamples(ifd, buf, realX, y, w, h);
    }

    return buf;
  }

  // -- Internal FormatReader API methods --

  /* @see loci.formats.FormatReader#initFile(String) */
  @Override
  protected void initFile(String id) throws FormatException, IOException {
    super.initFile(id);

    List<String> uniqueChannels = new ArrayList<String>();
    final List<Double> uniqueZs = new ArrayList<Double>();
    final List<Length> stageX = new ArrayList<Length>();
    final List<Length> stageY = new ArrayList<Length>();

    CoreMetadata m = core.get(0, 0);

    String filename = id.substring(id.lastIndexOf(File.separator) + 1);
    filename = filename.substring(0, filename.indexOf('.'));
    boolean integerFilename = true;
    try {
      Integer.parseInt(filename);
    }
    catch (NumberFormatException e) {
      integerFilename = false;
    }

    if (integerFilename && ifds.size() == 1 &&
      ifds.get(0).getIFDIntValue(IFD.NEW_SUBFILE_TYPE) == 2)
    {
      // look for other files in the dataset

      findTIFFs();

      String stageLabel = null;

      for (String tiff : files) {
        MetamorphHandler handler = new MetamorphHandler();
        parseFile(tiff, handler);

        String label = handler.getStageLabel();
        if (stageLabel == null) {
          stageLabel = label;
        }
        else if (!label.equals(stageLabel)) {
          break;
        }

        if (!uniqueChannels.contains(handler.getChannelName())) {
          uniqueChannels.add(handler.getChannelName());
        }

        final List<Double> zPositions = handler.getZPositions();
        Double pos = Math.rint(zPositions.get(0));

        if (!uniqueZs.contains(pos)) {
          uniqueZs.add(pos);
        }
      }

      MetamorphHandler handler = new MetamorphHandler();
      parseFile(files[files.length - 1], handler);

      String lastStageLabel = handler.getStageLabel();
      if (lastStageLabel!= null && stageLabel != null) {
        int lastField = getField(lastStageLabel);
        int lastWellRow = getWellRow(lastStageLabel);
        int lastWellColumn = getWellColumn(lastStageLabel);
  
        int field = getField(stageLabel);
        int fieldRow = getWellRow(stageLabel);
        int fieldColumn = getWellColumn(stageLabel);
  
        wellCount = lastField - field + 1;
        fieldRowCount = lastWellRow - fieldRow + 1;
        fieldColumnCount = lastWellColumn - fieldColumn + 1;
      }
      m.sizeC = uniqueChannels.size();
      m.sizeZ = uniqueZs.size();
    }
    else {
      files = new String[] {id};
      wellCount = 1;
      fieldRowCount = 1;
      fieldColumnCount = 1;
      m.sizeC = 0;
    }

    // parse XML comment

    MetamorphHandler handler = new MetamorphHandler(getGlobalMetadata());
    final List<Length> xPositions = new ArrayList<Length>();
    final List<Length> yPositions = new ArrayList<Length>();

    for (IFD ifd : ifds) {
      String xml = XMLTools.sanitizeXML(ifd.getComment());
      XMLTools.parseXML(xml, handler);

      final Length x = handler.getStagePositionX();
      final Length y = handler.getStagePositionY();

      if (xPositions.isEmpty()) {
        xPositions.add(x);
        yPositions.add(y);
      }
      else if (x != null && y != null) {
        final Length previousX = xPositions.get(xPositions.size() - 1);
        final Length previousY = yPositions.get(yPositions.size() - 1);

        final double x1 = x.value(UNITS.REFERENCEFRAME).doubleValue();
        final double x2 = previousX.value(UNITS.REFERENCEFRAME).doubleValue();
        final double y1 = y.value(UNITS.REFERENCEFRAME).doubleValue();
        final double y2 = previousY.value(UNITS.REFERENCEFRAME).doubleValue();

        if (Math.abs(x1 - x2) > 0.21 || Math.abs(y1 - y2) > 0.21) {
          xPositions.add(x);
          yPositions.add(y);
        }
      }
    }

    if (xPositions.size() > 1) {
      fieldRowCount = xPositions.size();
    }

    final List<Integer> wavelengths = handler.getWavelengths();
    final List<Double> zPositions = handler.getZPositions();
    dualCamera = handler.hasDualCamera();

    // calculate axis sizes

    final List<Integer> uniqueC = new ArrayList<Integer>();
    for (Integer c : wavelengths) {
      if (!uniqueC.contains(c)) {
        uniqueC.add(c);
      }
    }
    int effectiveC = uniqueC.size();
    if (effectiveC == 0) effectiveC = 1;
    if (getSizeC() == 0) m.sizeC = 1;
    int samples = ifds.get(0).getSamplesPerPixel();
    m.sizeC *= effectiveC * samples;

    final List<Double> uniqueZ = new ArrayList<Double>();
    for (Double z : zPositions) {
      if (!uniqueZ.contains(z)) uniqueZ.add(z);
    }

    if (getSizeZ() == 0) m.sizeZ = 1;
    if (uniqueZ.size() > 1) m.sizeZ *= uniqueZ.size();

    Double physicalSizeZ = null;
    if (m.sizeZ > 1) {
      Double zRange = zPositions.get(zPositions.size() - 1) - zPositions.get(0);
      physicalSizeZ = Math.abs(zRange);
      physicalSizeZ /= (m.sizeZ - 1);
    }

    int totalPlanes = files.length * ifds.size();
    effectiveC = getSizeC() / samples;

    // if the channel name and Z position are unique
    // for each plane, then prefer unique channels over unique Zs
    // the division by uniqueC.size is not a typo - it's meant to
    // account for the multiple actual Z sections
    if (effectiveC * getSizeZ() > totalPlanes &&
      (effectiveC * (getSizeZ() / uniqueC.size()) == totalPlanes ||
      effectiveC == totalPlanes))
    {
      if (getSizeZ() >= uniqueC.size()) {
        m.sizeZ /= uniqueC.size();
      }
      else {
        m.sizeZ = 1;
      }
    }

    m.sizeT = totalPlanes /
      (wellCount * fieldRowCount * fieldColumnCount * getSizeZ() * effectiveC);
    if (getSizeT() == 0) m.sizeT = 1;

    int seriesCount = wellCount * fieldRowCount * fieldColumnCount;

    if (seriesCount > 1 && getSizeZ() > totalPlanes / seriesCount ||
      totalPlanes > getSizeZ() * getSizeT() * effectiveC)
    {
      m.sizeZ = 1;
      m.sizeT = totalPlanes / (seriesCount * effectiveC);
    }

    m.imageCount = getSizeZ() * getSizeT() * effectiveC;

    if (dualCamera) {
      m.sizeX /= 2;
      m.sizeC *= 2;
      m.imageCount *= 2;
    }

    if (seriesCount > 1) {
      core.clear();
      for (int i=0; i<seriesCount; i++) {
        core.add(m);
      }
    }

    for (int s=0; s<wellCount * fieldRowCount * fieldColumnCount; s++) {
      if (files.length > 1) {
        int[] lengths = new int[] {getSizeZ(), getEffectiveSizeC(),
          fieldColumnCount, fieldRowCount, wellCount, getSizeT()};

        Well well = getWell(s);
        int[] position =
          new int[] {0, 0, well.fieldCol, well.fieldRow, well.well, 0};

        int fileIndex = FormatTools.positionToRaster(lengths, position);
        parseFile(files[fileIndex], handler);

        stageX.add(handler.getStagePositionX());
        stageY.add(handler.getStagePositionY());
      }
      else {
        stageX.add(xPositions.get(s));
        stageY.add(yPositions.get(s));
      }
    }

    MetadataStore store = makeFilterMetadata();
    MetadataTools.populatePixels(store, this);

    // Only populate Plate/Well metadata if there are multiple wells.
    // The presence of just one well suggests that we don't actually have
    // an HCS dataset (and even if we did, the Plate/Well metadata would be
    // effectively useless).
    if (wellCount > 1) {
      store.setPlateID(MetadataTools.createLSID("Plate", 0), 0);
      store.setPlateRows(new PositiveInteger(1), 0);
      store.setPlateColumns(new PositiveInteger(wellCount), 0);
      store.setPlateRowNamingConvention(NamingConvention.LETTER, 0);
      store.setPlateColumnNamingConvention(NamingConvention.NUMBER, 0);

      store.setPlateAcquisitionID(
        MetadataTools.createLSID("PlateAcquisition", 0, 0), 0, 0);

      for (int well=0; well<wellCount; well++) {
        store.setWellID(MetadataTools.createLSID("Well", 0, well), 0, well);
        store.setWellRow(new NonNegativeInteger(0), 0, well);
        store.setWellColumn(new NonNegativeInteger(well), 0, well);

        for (int row=0; row<fieldRowCount; row++) {
          for (int col=0; col<fieldColumnCount; col++) {
            int field = row * fieldColumnCount + col;
            String wellSampleID =
              MetadataTools.createLSID("WellSample", 0, well, field);
            store.setWellSampleID(wellSampleID, 0, well, field);

            int seriesIndex = getSeriesIndex(row, col, well);
            String imageID = MetadataTools.createLSID("Image", seriesIndex);
            store.setImageID(imageID, seriesIndex);
            store.setWellSampleImageRef(imageID, 0, well, field);
            store.setWellSampleIndex(
              new NonNegativeInteger(seriesIndex), 0, well, field);
            store.setPlateAcquisitionWellSampleRef(wellSampleID, 0, 0, seriesIndex);
          }
        }
      }
    }

    final List<String> timestamps = handler.getTimestamps();
    final List<Double> exposures = handler.getExposures();
    if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
      for (int i=0; i<timestamps.size(); i++) {
        long timestamp = DateTools.getTime(timestamps.get(i), DATE_FORMAT, ".");
        addGlobalMetaList("timestamp", timestamp);
      }
      for (int i=0; i<exposures.size(); i++) {
        Double exp = exposures.get(i);
        if (exp != null) {
          exp *= 1000;
        }

        addGlobalMetaList("exposure time (ms)", exp);
      }
    }

    for (int s=0; s<seriesCount; s++) {
      setSeries(s);
      Well well = getWell(s);

      String name = handler.getImageName();
      if (seriesCount > 1) {
        name = "Field " + FormatTools.getWellRowName(well.fieldRow) +
          (well.fieldCol + 1) + ", Well " + (well.well + 1) + ": " + name;
      }

      store.setImageName(name, s);

      String date =
        DateTools.formatDate(handler.getDate(), DateTools.ISO8601_FORMAT, ".");
      if (date != null) {
        store.setImageAcquisitionDate(new Timestamp(date), s);
      }

      if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
        long startDate = 0;
        if (timestamps.size() > 0) {
          startDate = DateTools.getTime(timestamps.get(0), DATE_FORMAT, ".");
        }

        store.setImageDescription("", s);

        int image = 0;
        for (int c=0; c<getEffectiveSizeC(); c++) {
          for (int t=0; t<getSizeT(); t++) {
            store.setPlaneTheZ(new NonNegativeInteger(0), s, image);
            store.setPlaneTheC(new NonNegativeInteger(c), s, image);
            store.setPlaneTheT(new NonNegativeInteger(t), s, image);

            if (t < timestamps.size()) {
              String stamp = timestamps.get(t);
              long ms = DateTools.getTime(stamp, DATE_FORMAT, ".");
              store.setPlaneDeltaT(new Time((ms - startDate) / 1000.0, UNITS.SECOND), s, image);
            }
            int exposureIndex = image;
            if (dualCamera) {
              exposureIndex /= getEffectiveSizeC();
            }
            if (exposures.size() == 1) {
              exposureIndex = 0;
            }
            if (exposureIndex < exposures.size() && exposures.get(exposureIndex) != null) {
              store.setPlaneExposureTime(new Time(exposures.get(exposureIndex), UNITS.SECOND), s, image);
            }
            if (s < stageX.size()) {
              store.setPlanePositionX(stageX.get(s), s, image);
            }
            if (s < stageY.size()) {
              store.setPlanePositionY(stageY.get(s), s, image);
            }
            image++;
          }
        }

        store.setImagingEnvironmentTemperature(
                new Temperature(handler.getTemperature(), UNITS.CELSIUS), s);

        Length sizeX =
          FormatTools.getPhysicalSizeX(handler.getPixelSizeX());
        Length sizeY =
          FormatTools.getPhysicalSizeY(handler.getPixelSizeY());
        Length sizeZ = physicalSizeZ == null ? null : 
          FormatTools.getPhysicalSizeZ(physicalSizeZ);

        if (sizeX != null) {
          store.setPixelsPhysicalSizeX(sizeX, s);
        }
        if (sizeY != null) {
          store.setPixelsPhysicalSizeY(sizeY, s);
        }
        if (sizeZ != null) {
          store.setPixelsPhysicalSizeZ(sizeZ, s);
        }

        if (uniqueChannels.size() == 0) {
          uniqueChannels = handler.getChannelNames();
        }

        for (int c=0; c<getEffectiveSizeC(); c++) {
          if (uniqueChannels.size() > c) {
            store.setChannelName(uniqueChannels.get(c), s, c);
          }
          else store.setChannelName(handler.getChannelName(), s, c);
          if (c < wavelengths.size()) {
            store.setChannelEmissionWavelength(
              FormatTools.getEmissionWavelength(Double.valueOf(wavelengths.get(c))), s, c);
          }
        }
      }
    }
  }

  // -- Helper methods --

  private int getSeriesIndex(int fieldRow, int fieldColumn, int well) {
    return FormatTools.positionToRaster(
      new int[] {fieldColumnCount, fieldRowCount, wellCount},
      new int[] {fieldColumn, fieldRow, well});
  }

  private Well getWell(int seriesIndex) {
    int[] coordinates = FormatTools.rasterToPosition(
      new int[] {fieldColumnCount, fieldRowCount, wellCount}, seriesIndex);
    return new Well(coordinates[1], coordinates[0], coordinates[2]);
  }

  private int getField(String stageLabel) {
    if (stageLabel.indexOf("Scan") < 0) return 0;
    String index = stageLabel.substring(0, stageLabel.indexOf(':')).trim();
    return Integer.parseInt(index) - 1;
  }

  private int getWellRow(String stageLabel) {
    int scanIndex = stageLabel.indexOf("Scan");
    if (scanIndex < 0) return 0;
    scanIndex = stageLabel.indexOf(" ", scanIndex) + 1;
    return (int) (stageLabel.charAt(scanIndex) - 'A');
  }

  private int getWellColumn(String stageLabel) {
    int scanIndex = stageLabel.indexOf("Scan");
    if (scanIndex < 0) return 0;
    scanIndex = stageLabel.indexOf(" ", scanIndex) + 2;
    return Integer.parseInt(stageLabel.substring(scanIndex)) - 1;
  }

  private void findTIFFs() throws IOException {
    Location baseFile = new Location(currentId).getAbsoluteFile();
    Location parent = baseFile.getParentFile();
    FilePattern pattern = new FilePattern(baseFile);
    String[] tiffs = pattern.getFiles();
    NumericComparator comparator = new NumericComparator();
    Arrays.sort(tiffs, comparator);

    final List<String> validTIFFs = new ArrayList<String>();

    for (String tiff : tiffs) {
      if (!new Location(tiff).exists()) {
        String base = tiff.substring(tiff.lastIndexOf(File.separator) + 1);
        base = base.substring(0, base.indexOf('.'));
        String suffix = tiff.substring(tiff.lastIndexOf("."));
        while (base.length() < 3) {
          base = "0" + base;
        }
        Location test = new Location(parent, base + suffix);
        if (test.exists()) {
          tiff = test.getAbsolutePath();
        }
        else continue;
      }
      if (File.separator.equals("\\")) {
        tiff = tiff.replaceAll("\\\\\\\\", "\\\\");
      }
      validTIFFs.add(tiff);
    }

    files = validTIFFs.toArray(new String[validTIFFs.size()]);
  }

  private void parseFile(String tiff, MetamorphHandler handler)
    throws IOException
  {
      try (RandomAccessInputStream s = new RandomAccessInputStream(tiff)) {
        TiffParser parser = new TiffParser(s);
        IFD firstIFD = parser.getFirstIFD();
        String xml = XMLTools.sanitizeXML(firstIFD.getComment());
        XMLTools.parseXML(xml, handler);
      }
  }

  // -- Helper classes --

  class Well {
    public int well;
    public int fieldRow;
    public int fieldCol;

    public Well(int fieldRow, int fieldCol, int well) {
      this.fieldRow = fieldRow;
      this.fieldCol = fieldCol;
      this.well = well;
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof Well)) return false;
      Well w = (Well) o;

      return w.well == this.well && w.fieldRow == this.fieldRow &&
        w.fieldCol == this.fieldCol;
    }

    @Override
    public int hashCode() {
      return (well << 16) | (fieldRow << 8) | fieldCol;
    }
  }

  class NumericComparator implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
      if (s1.equals(s2)) return 0;

      String base1 = s1.substring(s1.lastIndexOf(File.separator) + 1);
      String base2 = s2.substring(s2.lastIndexOf(File.separator) + 1);

      base1 = base1.substring(0, base1.indexOf('.'));
      base2 = base2.substring(0, base2.indexOf('.'));

      try {
        int num1 = Integer.parseInt(base1);
        int num2 = Integer.parseInt(base2);

        if (num1 == num2) return 0;
        return num1 < num2 ? -1 : 1;
      }
      catch (NumberFormatException e) { }
      return 0;
    }
  }

}
