/*
 * #%L
 * OME Bio-Formats manual and automated test suite.
 * %%
 * Copyright (C) 2006 - 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.tests.testng;

import static org.testng.AssertJUnit.fail;

import java.io.File;

import loci.common.Location;
import loci.formats.ChannelFiller;
import loci.formats.ChannelSeparator;
import loci.formats.FormatTools;
import loci.formats.IFormatReader;
import loci.formats.ImageReader;
import loci.formats.MinMaxCalculator;
import loci.formats.ReaderWrapper;
import nl.javadude.assumeng.Assumption;
import nl.javadude.assumeng.AssumptionListener;

import org.perf4j.StopWatch;
import org.perf4j.slf4j.Slf4JStopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Listeners;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;

/**
 * Performs various <code>openBytes()</code> performance tests.
 *
 * @author Chris Allan <callan at blackcat dot ca>
 */
@Listeners(value = AssumptionListener.class)
public class OpenBytesPerformanceTest
{
  private static final Logger LOGGER =
      LoggerFactory.getLogger(OpenBytesPerformanceTest.class);

  private String id;

  private IFormatReader reader;

  private int imageCount;

  private int seriesCount;

  private int sizeX;

  private int sizeY;

  private int sizeZ;

  private int sizeC;

  private int sizeT;

  private int bpp;

  private int topHalfSize;

  private int bottomHalfSize;

  private int topLeftQuarterSize;

  private int topRightQuarterSize;

  private int bottomLeftQuarterSize;

  private int bottomRightQuarterSize;

  private int optimalTileHeight;

  private int optimalTileWidth;

  private int planeSize;

  private String filename;

  private boolean memMap;

  private boolean bigImage = false;

  private void assertBlock(int blockSize, int posX, int posY, int width,
          int height) throws Exception {
    byte[] plane = new byte[planeSize];
    byte[] buf = new byte[blockSize];
    String planeDigest, bufDigest;
    for (int i = 0; i < imageCount; i++) {
        // Read the data as a full plane
        reader.openBytes(i, plane);
        // Read the data as a block
        try {
            reader.openBytes(i, buf, posX, posY, width, height);
        } catch (Exception e) {
            throw new RuntimeException(
                String.format(
                    "openBytes(series:%d i:%d, buf.length:%d, x:%d, y:%d, w:%d, h:%d) "
                                        + "[sizeX: %d sizeY:%d bpp:%d] threw exception!",
                            reader.getSeries(), i, buf.length, posX, posY,
                            width, height, sizeX, sizeY, bpp), e);
        }
        // Compare hash digests
        planeDigest = TestTools.md5(plane, sizeX, sizeY, posX, posY, width,
                height, bpp);
        bufDigest = TestTools.md5(buf, 0, width * height * bpp);
        if (!planeDigest.equals(bufDigest)) {
            fail(String.format("MD5:%d;%d len:%d %s != %s",
                    reader.getSeries(), i, blockSize, planeDigest,
                    bufDigest));
        }
    }
  }

  private void assertRows(int blockSize) throws Exception {
    for (int series = 0; series < seriesCount; series++) {
        assertSeries(series);
        byte[] plane = new byte[planeSize];
        byte[] buf = new byte[blockSize];
        int maximumRowCount = buf.length / bpp / sizeX;
        String planeDigest, bufDigest;
        int pixelsToRead = sizeX * sizeY;
        int width = sizeX, height, posX = 0, posY = 0, actualBufSize;
        for (int i = 0; i < imageCount; i++) {
          // Read the data as a full plane
          reader.openBytes(i, plane);
          int offset = 0;
          while(pixelsToRead > 0) {
            // Prepare our read metadata
            height = maximumRowCount;
            if ((posY + height) > sizeY)
            {
              height = sizeY - posY;
            }
            actualBufSize = bpp * height * width;
            // Read the data as a block
            try {
              reader.openBytes(i, buf, posX, posY, width, height);
            }
            catch (Exception e) {
              throw new RuntimeException(String.format(
                  "openBytes(series:%d i:%d, buf.length:%d, x:%d, y:%d, w:%d, " +
                  "h:%d) [sizeX: %d sizeY:%d bpp:%d] threw exception!",
                  series, i, buf.length, posX, posY, width, height, sizeX, sizeY,
                  bpp), e);
            }
            // Compare hash digests
            planeDigest = TestTools.md5(plane, offset, actualBufSize);
            bufDigest = TestTools.md5(buf, 0, actualBufSize);
            if (!planeDigest.equals(bufDigest)) {
              fail(String.format("MD5:%s;%d offset:%d len:%d %s != %s",
                  series, i, offset, actualBufSize, planeDigest, bufDigest));
            }
            // Update offsets, etc.
            offset += actualBufSize;
            posY += height;
            pixelsToRead -= height * width;
          }
        }
      }
    }

  private void assertSeries(int series) {
    reader.setSeries(series);
    sizeX = reader.getSizeX();
    sizeY = reader.getSizeY();
    sizeZ = reader.getSizeZ();
    sizeC = reader.getSizeC();
    sizeT = reader.getSizeT();
    imageCount = reader.getImageCount();
    bpp = FormatTools.getBytesPerPixel(reader.getPixelType());
    planeSize = sizeX * sizeY * bpp;
    topHalfSize = (sizeY / 2) * sizeX * bpp;
    bottomHalfSize = (sizeY - (sizeY / 2)) * sizeX * bpp;
    topLeftQuarterSize = (sizeY / 2) * (sizeX / 2) * bpp;
    topRightQuarterSize = (sizeY / 2) * (sizeX - (sizeX / 2)) * bpp;
    bottomLeftQuarterSize = (sizeY - (sizeY / 2)) * (sizeX / 2) * bpp;
    bottomRightQuarterSize =
      (sizeY - (sizeY / 2)) * (sizeX - (sizeX / 2)) * bpp;
    if (!bigImage) {
        bigImage = sizeX * sizeY > 9000000;
    }
  }

  public boolean isNotBigImage() {
      return !bigImage;
  }

  @Parameters({"id", "inMemory"})
  @BeforeClass
  public void init(String id, String inMemory) throws Exception {
    this.id = id;
    filename = new File(id).getName();
    memMap = Boolean.parseBoolean(inMemory);
  }

  @AfterClass
  public void tearDown() throws Exception {
    Location.mapId(id, null);
    reader.close();
  }

  @Test
  public void setId() throws Exception {
    reader = new ImageReader();
    reader = new ChannelFiller(reader);
    reader = new ChannelSeparator(reader);
    reader = new MinMaxCalculator(reader);

    if (memMap && reader.isSingleFile(id)) {
      TestTools.mapFile(id);
    }

    StopWatch stopWatch = new Slf4JStopWatch();
    reader.setId(id);
    stopWatch.stop(String.format("%s.setId.%s",
            ((ReaderWrapper) reader).unwrap().getClass().getName(), filename));
    seriesCount = reader.getSeriesCount();
  }

  @Test(dependsOnMethods={"setId"})
  public void testOpenBytesAllTilesNewBuffer() throws Exception {
    for (int series = 0; series < seriesCount; series++) {
      assertSeries(series);
      for (int image = 0; image < imageCount; image++) {
        LOGGER.info("Reading from series {} image {}", series, image);
        optimalTileWidth = reader.getOptimalTileWidth();
        optimalTileHeight = reader.getOptimalTileHeight();
        LOGGER.info("Optimal tile {}x{}", optimalTileWidth, optimalTileHeight);
        int tilesWide = (int) Math.ceil((double) sizeX / optimalTileWidth);
        int tilesHigh = (int) Math.ceil((double) sizeY / optimalTileHeight);
        LOGGER.info("Tile counts {}x{}", tilesWide, tilesHigh);
        int x, y = 0;
        StopWatch stopWatch;
        for (int tileX = 0; tileX < tilesWide; tileX++) {
          for (int tileY = 0; tileY < tilesHigh; tileY++) {
            x = tileX * optimalTileWidth;
            y = tileY * optimalTileHeight;

            int actualTileWidth =
              (int) Math.min(optimalTileWidth, reader.getSizeX() - x);
            int actualTileHeight =
              (int) Math.min(optimalTileHeight, reader.getSizeY() - y);

            LOGGER.info("Reading tile at {}x{}", x, y);
            stopWatch = new Slf4JStopWatch(String.format(
                "%s.alloc_tile.%s.[%d:%d]",
                ((ReaderWrapper) reader).unwrap().getClass().getName(),
                filename, series, image));
            reader.openBytes(0, x, y, actualTileWidth, actualTileHeight);
            stopWatch.stop();
          }
        }
      }
    }
  }

  @Test(dependsOnMethods={"setId"})
  @Assumption(methods = "isNotBigImage")
  public void testOpenBytesAllTilesPreAllocatedBuffer() throws Exception {
    for (int series = 0; series < seriesCount; series++) {
      assertSeries(series);
      for (int image = 0; image < imageCount; image++) {
        LOGGER.info("Reading from series {} image {}", series, image);
        optimalTileWidth = reader.getOptimalTileWidth();
        optimalTileHeight = reader.getOptimalTileHeight();
        LOGGER.info("Optimal tile {}x{}", optimalTileWidth, optimalTileHeight);
        int tilesWide = (int) Math.ceil((double) sizeX / optimalTileWidth);
        int tilesHigh = (int) Math.ceil((double) sizeY / optimalTileHeight);
        LOGGER.info("Tile counts {}x{}", tilesWide, tilesHigh);
        int x, y = 0;
        StopWatch stopWatch;
        byte[] buf = new byte[optimalTileWidth * optimalTileHeight *
          FormatTools.getBytesPerPixel(reader.getPixelType())];
        LOGGER.info("Allocated buffer size: {}", buf.length);
        for (int tileX = 0; tileX < tilesWide; tileX++) {
          for (int tileY = 0; tileY < tilesHigh; tileY++) {
            x = tileX * optimalTileWidth;
            y = tileY * optimalTileHeight;

            int actualTileWidth =
              (int) Math.min(optimalTileWidth, reader.getSizeX() - x);
            int actualTileHeight =
              (int) Math.min(optimalTileHeight, reader.getSizeY() - y);

            LOGGER.info("Reading tile at {}x{}", x, y);
            stopWatch = new Slf4JStopWatch(String.format(
                "%s.prealloc_tile.%s.[%d:%d]",
                ((ReaderWrapper) reader).unwrap().getClass().getName(),
                filename, series, image));
            reader.openBytes(image, buf, x, y, actualTileWidth,
                             actualTileHeight);
            stopWatch.stop();
          }
        }
      }
    }
  }

  @Test(dependsOnMethods={"setId"})
  @Assumption(methods = "isNotBigImage")
  public void testOpenBytesPlane() throws Exception {
    for (int series = 0; series < seriesCount; series++) {
      assertSeries(series);
      byte[] plane = new byte[planeSize];
      for (int i = 0; i < reader.getImageCount(); i++) {
        reader.openBytes(i, plane);
      }
    }
  }

  @Test(dependsOnMethods={"setId"})
  @Assumption(methods = "isNotBigImage")
  public void testOpenBytesHalfPlane() throws Exception {
    for (int series = 0; series < seriesCount; series++) {
      assertSeries(series);
      byte[] plane = new byte[planeSize];
      byte[] topHalfPlane = new byte[topHalfSize];
      byte[] bottomHalfPlane = new byte[bottomHalfSize];
      String planeDigest, halfPlaneDigest;
      for (int i = 0; i < imageCount; i++) {
        // Check the digest for the first half of the plane against a full
        // plane
        reader.openBytes(i, plane);
        reader.openBytes(i, topHalfPlane, 0, 0, sizeX, sizeY / 2);
        planeDigest = TestTools.md5(plane, 0, topHalfSize);
        halfPlaneDigest = TestTools.md5(topHalfPlane, 0, topHalfSize);
        if (!planeDigest.equals(halfPlaneDigest)) {
          fail(String.format("First half MD5:%d;%d %s != %s",
              series, i, planeDigest, halfPlaneDigest));
        }
        // Check the digest for the second half of the plane against a full
        // plane
        reader.openBytes(i, bottomHalfPlane, 0, sizeY / 2, sizeX,
            sizeY - (sizeY / 2));
        planeDigest = TestTools.md5(plane, topHalfSize, bottomHalfSize);
        halfPlaneDigest = TestTools.md5(bottomHalfPlane, 0, bottomHalfSize);
        if (!planeDigest.equals(halfPlaneDigest)) {
          fail(String.format("Second half MD5:%d;%d %s != %s",
              series, i, planeDigest, halfPlaneDigest));
        }
      }
    }
  }

  @Test(dependsOnMethods={"setId"})
  @Assumption(methods = "isNotBigImage")
  public void testQuartersActualSize() throws Exception {
    for (int series = 0; series < seriesCount; series++) {
      assertSeries(series);
      assertBlock(topLeftQuarterSize, 0, 0, sizeX / 2, sizeY / 2);
      assertBlock(topRightQuarterSize, sizeX / 2, 0,
                  sizeX - (sizeX / 2), sizeY / 2);
      assertBlock(bottomLeftQuarterSize, 0, sizeY / 2,
                  sizeX / 2, (sizeY - (sizeY / 2)));
      assertBlock(bottomRightQuarterSize, sizeX / 2, sizeY / 2,
                  sizeX - (sizeX / 2), sizeY - (sizeY / 2));
    }
  }

  @Test(dependsOnMethods={"setId"})
  @Assumption(methods = "isNotBigImage")
  public void testQuartersTwiceActualSize() throws Exception {
    for (int series = 0; series < seriesCount; series++) {
      assertSeries(series);
      assertBlock(topLeftQuarterSize * 2, 0, 0, sizeX / 2, sizeY / 2);
      assertBlock(topRightQuarterSize * 2, sizeX / 2, 0,
                  sizeX - (sizeX / 2), sizeY / 2);
      assertBlock(bottomLeftQuarterSize * 2, 0, sizeY / 2,
                  sizeX / 2, (sizeY - (sizeY / 2)));
      assertBlock(bottomRightQuarterSize * 2, sizeX / 2, sizeY / 2,
                  sizeX - (sizeX / 2), sizeY - (sizeY / 2));
    }
  }

  @Test(dependsOnMethods={"setId"})
  @Assumption(methods = "isNotBigImage")
  public void testOpenBytesBlocksByRow512KB() throws Exception {
    assertRows(524288);
  }

  @Test(dependsOnMethods={"setId"})
  @Assumption(methods = "isNotBigImage")
  public void testOpenBytesBlocksByRow1MB() throws Exception {
    assertRows(1048576);
  }

  @Test(dependsOnMethods={"setId"})
  @Assumption(methods = "isNotBigImage")
  public void testOpenBytesBlocksByRowPlaneSize() throws Exception {
    assertRows(sizeX * sizeY * bpp);
  }

}
