/*
 * #%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.IOException;
import java.util.Arrays;
import java.util.ArrayList;

import loci.common.ByteArrayHandle;
import loci.common.DataTools;
import loci.common.IRandomAccess;
import loci.common.Location;
import loci.common.RandomAccessInputStream;
import loci.common.services.DependencyException;
import loci.common.services.ServiceFactory;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.MissingLibraryException;
import loci.formats.codec.LZOCodec;
import loci.formats.meta.MetadataStore;
import loci.formats.services.MetakitService;

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

/**
 * VolocityReader is the file format reader for Volocity library files.
 *
 * @author Melissa Linkert melissa at glencoesoftware.com
 */
public class VolocityReader extends FormatReader {

  // -- Constants --

  private static final String DATA_DIR = "Data";
  private static final String EMBEDDED_STREAM = "embedded-stream.raw";

  private static final int SIGNATURE_SIZE = 13;

  // -- Fields --

  private ArrayList<Stack> stacks;
  private ArrayList<String> extraFiles;
  private Object[][] sampleTable, stringTable;
  private String dir = null;

  private ArrayList<Double[]> timestamps = new ArrayList<Double[]>();

  // -- Constructor --

  /** Constructs a new Volocity reader. */
  public VolocityReader() {
    super("Volocity Library",
      new String[] {"mvd2", "aisf", "aiix", "dat", "atsf"});
    domains = new String[] {FormatTools.UNKNOWN_DOMAIN};
    hasCompanionFiles = true;
    datasetDescription = "One .mvd2 file plus a 'Data' directory";
  }

  // -- IFormatReader API methods --

  /* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */
  @Override
  public String[] getSeriesUsedFiles(boolean noPixels) {
    FormatTools.assertId(currentId, true, 1);

    ArrayList<String> files = new ArrayList<String>();
    files.addAll(extraFiles);
    if (!noPixels) {
      Stack stack = stacks.get(getSeries());
      for (int c=0; c<getEffectiveSizeC(); c++) {
        files.add(stack.pixelsFiles[c]);
      }
      if (stack.timestampFile != null) {
        files.add(stack.timestampFile);
      }
    }
    return files.toArray(new String[files.size()]);
  }

  /* @see loci.formats.IFormatReader#isThisType(String, boolean) */
  @Override
  public boolean isThisType(String name, boolean open) {
    if (checkSuffix(name, "mvd2")) {
      return super.isThisType(name, open);
    }

    if (open && checkSuffix(name, suffixes)) {
      Location file = new Location(name).getAbsoluteFile();
      Location parent = file.getParentFile();
      parent = parent.getParentFile();
      if (parent != null) {
        parent = parent.getParentFile();
        if (parent != null) {
          Location mvd2 = new Location(parent, parent.getName() + ".mvd2");
          return mvd2.exists() && super.isThisType(mvd2.getAbsolutePath());
        }
      }
    }
    return false;
  }

  /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
  @Override
  public boolean isThisType(RandomAccessInputStream stream) throws IOException {
    final int blockLen = 2;
    if (!FormatTools.validStream(stream, blockLen, false)) return false;
    String check = stream.readString(blockLen);
    return check.equals("JL") || check.equals("LJ");
  }

  /**
   * @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
  {
    FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h);

    int[] zct = getZCTCoords(no);

    Stack stack = stacks.get(getSeries());

    if (!new Location(stack.pixelsFiles[zct[1]]).exists()) {
      Arrays.fill(buf, getFillColor());
      return buf;
    }

    try (RandomAccessInputStream pix =
      new RandomAccessInputStream(stack.pixelsFiles[zct[1]])) {
      int padding = zct[2] * stack.planePadding;

      long planeSize = FormatTools.getPlaneSize(this);
      int planesInFile = (int) (pix.length() / planeSize);
      int planeIndex = no / getEffectiveSizeC();
      if (planesInFile == getSizeT()) {
        planeIndex = zct[2];

        int block = stack.blockSize;
        padding = block - (int) (planeSize % block);
        if (padding == block) {
          padding = 0;
        }
        padding *= zct[2];
      }

      long offset = (long) stack.blockSize + planeIndex * planeSize + padding;
      if (offset >= pix.length()) {
        return buf;
      }
      pix.seek(offset);

      if (stack.clippingData) {
        pix.seek(offset - 3);
        ByteArrayHandle v = new ByteArrayHandle();
        while (v.length() < FormatTools.getPlaneSize(this) &&
          pix.getFilePointer() < pix.length())
        {
          try {
            byte[] b = new LZOCodec().decompress(pix, null);
            pix.skipBytes(4);
            v.write(b);
          }
          catch (IOException e) { }
        }
        try (RandomAccessInputStream s = new RandomAccessInputStream(v)) {
          s.seek(0);
          readPlane(s, x, y, w, h, buf);
        }
      }
      else {
        if (pix.getFilePointer() + planeSize > pix.length()) {
          return buf;
        }
        readPlane(pix, x, y, w, h, buf);
      }
    }

    if (getRGBChannelCount() == 4) {
      // stored as ARGB, need to swap to RGBA
      for (int i=0; i<buf.length/4; i++) {
        byte a = buf[i * 4];
        buf[i * 4] = buf[i * 4 + 1];
        buf[i * 4 + 1] = buf[i * 4 + 2];
        buf[i * 4 + 2] = buf[i * 4 + 3];
        buf[i * 4 + 3] = a;
      }
    }

    return buf;
  }

  /* @see loci.formats.IFormatReader#close(boolean) */
  @Override
  public void close(boolean fileOnly) throws IOException {
    super.close(fileOnly);
    if (!fileOnly) {
      stacks = null;
      sampleTable = null;
      stringTable = null;
      dir = null;
      timestamps.clear();
      Location.mapFile(EMBEDDED_STREAM, null);
    }
  }

  // -- Internal FormatReader API methods --

  /* @see loci.formats.FormatReader#initFile(String) */
  @Override
  protected void initFile(String id) throws FormatException, IOException {
    if (!checkSuffix(id, "mvd2")) {
      Location file = new Location(id).getAbsoluteFile();
      Location parent = file.getParentFile().getParentFile();
      String[] files = parent.list(true);
      for (String f : files) {
        if (checkSuffix(f, "mvd2")) {
          id = new Location(parent, f).getAbsolutePath();
          break;
        }
      }
    }

    super.initFile(id);

    stacks = new ArrayList<Stack>();
    extraFiles = new ArrayList<String>();

    Location file = new Location(id).getAbsoluteFile();
    extraFiles.add(file.getAbsolutePath());

    Location parentDir = file.getParentFile();
    Location dataDir = new Location(parentDir, DATA_DIR);
    dir = dataDir.getAbsolutePath();

    if (dataDir.exists()) {
      String[] files = dataDir.list(true);
      for (String f : files) {
        if (!checkSuffix(f, "aisf") && !checkSuffix(f, "atsf")) {
          extraFiles.add(new Location(dataDir, f).getAbsolutePath());
        }
      }
    }

    try {
      ServiceFactory factory = new ServiceFactory();
      MetakitService reader = factory.getInstance(MetakitService.class);
      reader.initialize(id);
      sampleTable = reader.getTableData(1);
      stringTable = reader.getTableData(2);

      reader.close();
    }
    catch (DependencyException e) {
      throw new MissingLibraryException("Could not find Metakit library", e);
    }

    ArrayList<String> stackNames = new ArrayList<String>();
    ArrayList<Integer> parentIDs = new ArrayList<Integer>();

    for (int i=0; i<sampleTable.length; i++) {
      Integer stringID = (Integer) sampleTable[i][11];
      String name = getString(stringID);

      int channelIndex = getChildIndex((Integer) sampleTable[i][0], "Channels");

      if (i > 0 && (Integer) sampleTable[i][2] == 1 && (channelIndex >= 0 ||
        (sampleTable[i][14] != null && !sampleTable[i][14].equals(0)) ||
        ((byte[]) sampleTable[i][13]).length > 21))
      {
        String parentName = getParentName((Integer) sampleTable[i][1]);

        if (channelIndex < 0) {
          try (RandomAccessInputStream s = getStream(i)) {
            s.seek(0);
            if (s.read() != 'I') {
              s.order(false);
            }
            s.seek(22);
            int x = s.readInt();
            int y = s.readInt();
            int z = s.readInt();
            if (x * y * z > 0 && x * y * z < (s.length() * 3)) {
              stackNames.add(parentName + name);
              parentIDs.add((Integer) sampleTable[i][0]);
            }
          }
        }
        else {
          stackNames.add(parentName + name);
          parentIDs.add((Integer) sampleTable[i][0]);
        }
      }
    }

    for (int i=0; i<parentIDs.size(); i++) {
      Stack stack = new Stack();
      stack.core = new CoreMetadata();
      Integer parent = parentIDs.get(i);

      int channelIndex = getChildIndex(parent, "Channels");
      if (channelIndex >= 0) {
        Integer[] channels =
          getAllChildren((Integer) sampleTable[channelIndex][0]);
        stack.core.sizeC = channels.length;
        stack.pixelsFiles = new String[stack.core.sizeC];

        stack.channelNames = new String[channels.length];
        for (int c=0; c<channels.length; c++) {
          stack.channelNames[c] =
            getString((Integer) sampleTable[channels[c]][11]);

          try (RandomAccessInputStream data = getStream(channels[c])) {
            if (data.length() > 22) {
              data.seek(22);
              int stackID = data.readInt();
              Location f = new Location(dir, stackID + ".aisf");
              if (!f.exists()) {
                f = new Location(dir, DataTools.swap(stackID) + ".aisf");
              }
              stack.pixelsFiles[c] = f.getAbsolutePath();
            }
            else {
              Integer child =
                getAllChildren((Integer) sampleTable[channels[c]][0])[0];
                stack.pixelsFiles[c] =
                  getFile((Integer) sampleTable[child][0], dir);
            }
          }
        }
      }
      else {
        stack.pixelsFiles = new String[1];
        stack.pixelsFiles[0] = getFile(parent, dir);

        if (stack.pixelsFiles[0] == null ||
          !new Location(stack.pixelsFiles[0]).exists())
        {
          int row = -1;
          for (int r=0; r<sampleTable.length; r++) {
            if (sampleTable[r][0].equals(parent)) {
              row = r;
              break;
            }
          }

          stack.pixelsFiles[0] = EMBEDDED_STREAM;
          IRandomAccess data =
            new ByteArrayHandle((byte[]) sampleTable[row][13]);
          Location.mapFile(stack.pixelsFiles[0], data);
        }
      }

      int timestampIndex = getChildIndex(parent, "Timepoint times stream");
      if (timestampIndex >= 0) {
        try (RandomAccessInputStream data = getStream(timestampIndex)) {
          data.seek(22);
          int timestampID = data.readInt();
          Location f = new Location(dir, timestampID + ".atsf");
          if (!f.exists()) {
            f = new Location(dir, DataTools.swap(timestampID) + ".atsf");
          }
          stack.timestampFile = f.getAbsolutePath();
        }
      }

      int xIndex = getChildIndex(parent, "um/pixel (X)");
      if (xIndex >= 0) {
        try (RandomAccessInputStream data = getStream(xIndex)) {
          data.seek(SIGNATURE_SIZE);
          stack.physicalX = data.readDouble();
        }
      }

      int yIndex = getChildIndex(parent, "um/pixel (Y)");
      if (yIndex >= 0) {
        try (RandomAccessInputStream data = getStream(yIndex)) {
          data.seek(SIGNATURE_SIZE);
          stack.physicalY = data.readDouble();
        }
      }

      int zIndex = getChildIndex(parent, "um/pixel (Z)");
      if (zIndex >= 0) {
        try (RandomAccessInputStream data = getStream(zIndex)) {
          data.seek(SIGNATURE_SIZE);
          stack.physicalZ = data.readDouble();
        }
      }

      timestampIndex = getChildIndex(parent, "TimepointTimes");
      if (timestampIndex >= 0) {
        try (RandomAccessInputStream data = getStream(timestampIndex)) {
          data.seek(SIGNATURE_SIZE);
        }
      }

      int objectiveIndex = getChildIndex(parent, "Microscope Objective");
      if (objectiveIndex >= 0) {
        try (RandomAccessInputStream data = getStream(objectiveIndex)) {
          data.seek(SIGNATURE_SIZE);
          stack.magnification = data.readDouble();
        }
      }

      int detectorIndex = getChildIndex(parent, "Camera/Detector");
      if (detectorIndex >= 0) {
        try (RandomAccessInputStream data = getStream(detectorIndex)) {
          data.seek(SIGNATURE_SIZE);
          int len = data.readInt();
          stack.detector = data.readString(len);
        }
      }

      int descriptionIndex = getChildIndex(parent, "Experiment Description");
      if (descriptionIndex >= 0) {
        try (RandomAccessInputStream data = getStream(descriptionIndex)) {
          data.seek(SIGNATURE_SIZE);
          int len = data.readInt();
          stack.description = data.readString(len);
        }
      }

      int xLocationIndex = getChildIndex(parent, "X Location");
      if (xLocationIndex >= 0) {
        try (RandomAccessInputStream data = getStream(xLocationIndex)) {
          data.seek(SIGNATURE_SIZE);
          stack.xLocation = data.readDouble();
        }
      }

      int yLocationIndex = getChildIndex(parent, "Y Location");
      if (yLocationIndex >= 0) {
        try (RandomAccessInputStream data = getStream(yLocationIndex)) {
          data.seek(SIGNATURE_SIZE);
          stack.yLocation = data.readDouble();
        }
      }

      int zLocationIndex = getChildIndex(parent, "Z Location");
      if (zLocationIndex >= 0) {
        try (RandomAccessInputStream data = getStream(zLocationIndex)) {
          data.seek(SIGNATURE_SIZE);
          stack.zLocation = data.readDouble();
        }
      }

      stacks.add(stack);
    }

    // split up channels as necessary

    for (int i=0; i<stacks.size(); i++) {
      Stack stack = stacks.get(i);

      if (!new Location(stack.pixelsFiles[0]).exists()) {
        stacks.remove(i);
        i--;
        continue;
      }
      long baseLength = 0;
      try (RandomAccessInputStream base =
        new RandomAccessInputStream(stack.pixelsFiles[0])) {
        baseLength = base.length();
      }

      for (int q=1; q<stack.pixelsFiles.length; q++) {
        if (!new Location(stack.pixelsFiles[q]).exists()) {
          continue;
        }
        long length = 0;
        try (RandomAccessInputStream base = new RandomAccessInputStream(stack.pixelsFiles[q])) {
          length = base.length();
        }
        if (length > baseLength) {
          // split the stack
          Stack newStack = new Stack();

          newStack.timestampFile = stack.timestampFile;
          newStack.core = new CoreMetadata();
          newStack.physicalX = stack.physicalX;
          newStack.physicalY = stack.physicalY;
          newStack.physicalZ = stack.physicalZ;
          newStack.magnification = stack.magnification;
          newStack.detector = stack.detector;
          newStack.description = stack.description;
          newStack.xLocation = stack.xLocation;
          newStack.yLocation = stack.yLocation;
          newStack.zLocation = stack.zLocation;

          String[] pixels = stack.pixelsFiles;
          newStack.pixelsFiles = new String[pixels.length - q];
          System.arraycopy(pixels, q, newStack.pixelsFiles, 0,
            newStack.pixelsFiles.length);
          stack.pixelsFiles = new String[q];
          System.arraycopy(pixels, 0, stack.pixelsFiles, 0, q);

          String[] channels = stack.channelNames;
          newStack.channelNames = new String[channels.length - q];
          System.arraycopy(channels, q, newStack.channelNames, 0,
            newStack.channelNames.length);
          stack.channelNames = new String[q];
          System.arraycopy(channels, 0, stack.channelNames, 0, q);

          newStack.core.sizeC = newStack.channelNames.length;
          stack.core.sizeC = stack.channelNames.length;

          stacks.add(i + 1, newStack);
          stackNames.add(i + 1, stackNames.get(i));
        }
      }
    }

    int seriesCount = stacks.size();
    core.clear();
    for (int i=0; i<seriesCount; i++) {
      Stack stack = stacks.get(i);
      CoreMetadata ms = stack.core;
      core.add(ms);

      setSeries(i);

      ms.littleEndian = true;

      if (stack.timestampFile != null) {
        try (RandomAccessInputStream s =
          new RandomAccessInputStream(stack.timestampFile)) {
          s.seek(0);
          if (s.read() != 'I') {
            ms.littleEndian = false;
          }
          s.seek(17);
          s.order(isLittleEndian());
          ms.sizeT = s.readInt();

          Double firstStamp = null;
          Double[] stamps = new Double[ms.sizeT];
          for (int t=0; t<ms.sizeT; t++) {
            // timestamps are stored in microseconds
            double timestamp = s.readLong() / 1000000.0;
            if (firstStamp == null) {
              firstStamp = timestamp;
            }
            stamps[t] = timestamp - firstStamp;
          }
          timestamps.add(stamps);
        }
      }
      else {
        ms.sizeT = 1;
      }

      ms.rgb = false;
      ms.interleaved = true;
      ms.dimensionOrder = "XYCZT";

      try (RandomAccessInputStream s =
        new RandomAccessInputStream(stack.pixelsFiles[0])) {
          s.order(isLittleEndian());

          if (checkSuffix(stack.pixelsFiles[0], "aisf")) {
            s.seek(18);
            stack.blockSize = s.readShort() * 256;
            s.skipBytes(5);
            int x = s.readInt();
            int y = s.readInt();
            int zStart = s.readInt();
            int w = s.readInt();
            int h = s.readInt();

            if (w - x < 0 || h - y < 0 || (w - x) * (h - y) < 0) {
              ms.littleEndian = !isLittleEndian();
              s.order(isLittleEndian());
              s.seek(s.getFilePointer() - 20);
              x = s.readInt();
              y = s.readInt();
              zStart = s.readInt();
              w = s.readInt();
              h = s.readInt();
            }

            ms.sizeX = w - x;
            ms.sizeY = h - y;
            ms.sizeZ = s.readInt() - zStart;
            ms.imageCount = getSizeZ() * getSizeC() * getSizeT();
            ms.pixelType = FormatTools.INT8;

            int planesPerFile = getSizeZ() * getSizeT();
            int planeSize = FormatTools.getPlaneSize(this);
            int bytesPerPlane =
              (int) ((s.length() - stack.blockSize) / planesPerFile);

            int bytesPerPixel = 0;
            while (bytesPerPlane >= planeSize) {
              bytesPerPixel++;
              bytesPerPlane -= planeSize;
            }

            if ((bytesPerPixel % 3) == 0) {
              ms.sizeC *= 3;
              ms.rgb = true;
              bytesPerPixel /= 3;
            }

            ms.pixelType = FormatTools.pixelTypeFromBytes(
              bytesPerPixel, false, bytesPerPixel > 2);

            // full timepoints are padded to have a multiple of 256 bytes
            int timepoint = FormatTools.getPlaneSize(this) * getSizeZ();
            stack.planePadding = stack.blockSize - (timepoint % stack.blockSize);
            if (stack.planePadding == stack.blockSize) {
              stack.planePadding = 0;
            }
          }
          else {
            boolean embedded = Location.getMappedFile(EMBEDDED_STREAM) != null;

            s.seek(0);
            if (s.read() != 'I') {
              ms.littleEndian = false;
              s.order(false);
            }

            s.seek(22);
            ms.sizeX = s.readInt();
            ms.sizeY = s.readInt();
            ms.sizeZ = s.readInt();
            ms.sizeC = embedded ? 1 : 4;
            ms.imageCount = getSizeZ() * getSizeT();
            ms.rgb = ms.sizeC > 1;
            ms.pixelType = FormatTools.UINT8;
            stack.blockSize = embedded ? (int) s.getFilePointer() : 99;
            stack.planePadding = 0;

            if (s.length() > ms.sizeX * ms.sizeY * ms.sizeZ * 6) {
              ms.pixelType = FormatTools.UINT16;
              ms.sizeC = 3;
              ms.rgb = true;
            }

            if (s.length() <
              (ms.sizeX * ms.sizeY * ms.sizeZ * ms.sizeC))
            {
              ms.rgb = false;
              ms.sizeC = 1;
              long pixels = ms.sizeX * ms.sizeY * ms.sizeZ;
              double approximateBytes = (double) s.length() / pixels;
              int bytes = (int) Math.ceil(approximateBytes);
              if (bytes == 0) {
                bytes = 1;
              }
              else if (bytes == 3) {
                bytes = 2;
              }
              ms.pixelType =
                FormatTools.pixelTypeFromBytes(bytes, false, false);
              s.seek(70);
              stack.blockSize = s.readInt();
              stack.clippingData = true;
            }
          }
      }
    }
    setSeries(0);

    for (int i=0; i<getSeriesCount(); i++) {
      setSeries(i);

      Stack stack = stacks.get(i);

      addSeriesMeta("Name", stackNames.get(i));
      addSeriesMeta("Pixel width (in microns)", stack.physicalX);
      addSeriesMeta("Pixel height (in microns)", stack.physicalY);
      addSeriesMeta("Z step (in microns)", stack.physicalZ);
      addSeriesMeta("Objective magnification", stack.magnification);
      addSeriesMeta("Camera/Detector", stack.detector);
      addSeriesMeta("Description", stack.description);
      addSeriesMeta("X Location", stack.xLocation);
      addSeriesMeta("Y Location", stack.yLocation);
      addSeriesMeta("Z Location", stack.zLocation);

      if (stack.channelNames != null) {
        for (int c=0; c<stack.channelNames.length; c++) {
          addSeriesMetaList("Channel", stack.channelNames[c]);
        }
      }
    }
    setSeries(0);

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

    String instrument = MetadataTools.createLSID("Instrument", 0);
    store.setInstrumentID(instrument, 0);

    for (int i=0; i<getSeriesCount(); i++) {
      store.setImageInstrumentRef(instrument, i);

      setSeries(i);
      Stack stack = stacks.get(i);
      store.setImageName(stackNames.get(i), i);
      store.setImageDescription(stack.description, i);
      if (stack.channelNames != null) {
        for (int c=0; c<getEffectiveSizeC(); c++) {
          store.setChannelName(stack.channelNames[c], i, c);
        }
      }

      Length sizeX = FormatTools.getPhysicalSizeX(stack.physicalX);
      Length sizeY = FormatTools.getPhysicalSizeY(stack.physicalY);
      Length sizeZ = FormatTools.getPhysicalSizeZ(stack.physicalZ);
      if (sizeX != null) {
        store.setPixelsPhysicalSizeX(sizeX, i);
      }
      if (sizeY != null) {
        store.setPixelsPhysicalSizeY(sizeY, i);
      }
      if (sizeZ != null) {
        store.setPixelsPhysicalSizeZ(sizeZ, i);
      }

      String objective = MetadataTools.createLSID("Objective", 0, i);
      store.setObjectiveID(objective, 0, i);
      store.setObjectiveNominalMagnification(stack.magnification, 0, i);
      store.setObjectiveCorrection(MetadataTools.getCorrection("Other"), 0, i);
      store.setObjectiveImmersion(MetadataTools.getImmersion("Other"), 0, i);
      store.setObjectiveSettingsID(objective, i);

      String detectorID = MetadataTools.createLSID("Detector", 0, i);
      store.setDetectorID(detectorID, 0, i);
      store.setDetectorModel(stack.detector, 0, i);

      for (int c=0; c<getEffectiveSizeC(); c++) {
        store.setDetectorSettingsID(detectorID, i, c);
      }

      for (int img=0; img<getImageCount(); img++) {
        int[] coords = getZCTCoords(img);
        int z = coords[0];
        final Length xLoc = new Length(stack.xLocation, UNITS.REFERENCEFRAME);
        final Length yLoc = new Length(stack.yLocation, UNITS.REFERENCEFRAME);
        store.setPlanePositionX(xLoc, i, img);
        store.setPlanePositionY(yLoc, i, img);
        if (stack.physicalZ != null) {
          final double zLocNumber = stack.zLocation + z * stack.physicalZ;
          final Length zLoc = new Length(zLocNumber, UNITS.REFERENCEFRAME);
          store.setPlanePositionZ(zLoc, i, img);
        }

        if (i < timestamps.size() && coords[2] < timestamps.get(i).length && timestamps.get(i)[coords[2]] != null) {
          store.setPlaneDeltaT(new Time(timestamps.get(i)[coords[2]], UNITS.SECOND), i, img);
        }
      }
    }
    setSeries(0);
  }

  private String getParentName(Integer parentID) {
    String parentName = "";
    while (parentID != 1) {
      Integer originalID = parentID;
      for (int row=0; row<sampleTable.length; row++) {
        if (parentID.intValue() == ((Integer) sampleTable[row][0]).intValue()) {
          parentName = getString((Integer) sampleTable[row][11]) + "/" + parentName;
          parentID = (Integer) sampleTable[row][1];
          break;
        }
      }
      if (parentID.equals(originalID)) {
        break;
      }
    }
    return parentName;
  }

  private String getString(Integer stringID) {
    for (int row=0; row<stringTable.length; row++) {
      if (stringID.equals(stringTable[row][0])) {
        String s = (String) stringTable[row][1];
        if (s != null) {
          s = s.trim();
        }
        return s;
      }
    }
    return null;
  }

  private int getChildIndex(Integer parentID, String childName) {
    for (int row=0; row<sampleTable.length; row++) {
      if (parentID.equals(sampleTable[row][1])) {
        String name = getString((Integer) sampleTable[row][11]);
        if (childName.equals(name)) {
          return row;
        }
      }
    }
    return -1;
  }

  private Integer[] getAllChildren(Integer parentID) {
    ArrayList<Integer> children = new ArrayList<Integer>();
    for (int row=0; row<sampleTable.length; row++) {
      if (parentID.equals(sampleTable[row][1])) {
        children.add(row);
      }
    }
    return children.toArray(new Integer[children.size()]);
  }

  private RandomAccessInputStream getStream(int row) throws IOException {
    Object o = sampleTable[row][14];
    String fileLink = o == null ? "0" : o.toString().trim();
    RandomAccessInputStream data = null;
    if (fileLink.equals("0")) {
      data = new RandomAccessInputStream((byte[]) sampleTable[row][13]);
    }
    else {
      fileLink = new Location(dir, fileLink + ".dat").getAbsolutePath();
      data = new RandomAccessInputStream(fileLink);
    }

    data.order(true);
    return data;
  }

  private String getFile(Integer parent, String dir) {
    for (int row=0; row<sampleTable.length; row++) {
      if (parent.equals(sampleTable[row][0])) {
        Object o = sampleTable[row][14];
        if (o != null) {
          String fileLink = o.toString().trim() + ".dat";
          return new Location(dir, fileLink).getAbsolutePath();
        }
      }
    }
    return null;
  }

  // -- Helper class --

  class Stack {
    public String[] pixelsFiles;
    public String timestampFile;
    public int planePadding;
    public int blockSize;
    public boolean clippingData;

    public CoreMetadata core;
    public String[] channelNames;
    public Double physicalX;
    public Double physicalY;
    public Double physicalZ;
    public Double magnification;
    public String detector;
    public String description;
    public double xLocation;
    public double yLocation;
    public double zLocation;
  }

}
