/*
 * #%L
 * BSD implementations of Bio-Formats readers and writers
 * %%
 * Copyright (C) 2005 - 2017 Open Microscopy Environment:
 *   - Board of Regents of the University of Wisconsin-Madison
 *   - Glencoe Software, Inc.
 *   - University of Dundee
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

package loci.formats.in;

import java.io.IOException;
import java.util.Vector;

import loci.common.ByteArrayHandle;
import loci.common.DataTools;
import loci.common.RandomAccessInputStream;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.codec.CodecOptions;
import loci.formats.codec.JPEGCodec;
import loci.formats.codec.PackbitsCodec;
import loci.formats.gui.AWTImageTools;
import loci.formats.meta.MetadataStore;

/**
 * PictReader is the file format reader for Apple PICT files.
 * Most of this code was adapted from the PICT readers in JIMI
 * (http://java.sun.com/products/jimi/index.html), ImageMagick
 * (http://www.imagemagick.org), and Java QuickDraw.
 */
public class PictReader extends FormatReader {

  // -- Constants --

  // opcodes that we need
  private static final int PICT_CLIP_RGN = 1;
  private static final int PICT_BITSRECT = 0x90;
  private static final int PICT_BITSRGN = 0x91;
  private static final int PICT_PACKBITSRECT = 0x98;
  private static final int PICT_PACKBITSRGN = 0x99;
  private static final int PICT_9A = 0x9a;
  private static final int PICT_END = 0xff;
  private static final int PICT_LONGCOMMENT = 0xa1;
  private static final int PICT_JPEG = 0x18;
  private static final int PICT_TYPE_1 = 0xa9f;
  private static final int PICT_TYPE_2 = 0x9190;

  /** Table used in expanding pixels that use less than 8 bits. */
  private static final byte[] EXPANSION_TABLE = new byte[256 * 8];

  static {
    for (int i=0; i<256; i++) {
      for (int j=0; j<8; j++) {
        EXPANSION_TABLE[i*8 + j] =
          (byte) ((i & (int) Math.pow(2, 7 - j)) >> 7 - j);
      }
    }
  }

  // -- Fields --

  /** Number of bytes in a row of pixel data (variable). */
  protected int rowBytes;

  /** Vector of byte arrays representing individual rows. */
  protected Vector strips;

  /** Whether or not the file is PICT v1. */
  protected boolean versionOne;

  /** Color lookup table for palette color images. */
  protected byte[][] lookup;

  private Vector<Long> jpegOffsets = new Vector<Long>();

  // -- Constructor --

  /** Constructs a new PICT reader. */
  public PictReader() {
    super("PICT", new String[] {"pict", "pct"});
    domains = new String[] {FormatTools.GRAPHICS_DOMAIN};
  }

  // -- IFormatReader API methods --

  /* @see loci.formats.IFormatReader#get8BitLookupTable() */
  @Override
  public byte[][] get8BitLookupTable() throws FormatException, IOException {
    FormatTools.assertId(currentId, true, 1);
    return lookup;
  }

  /**
   * @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);

    if (jpegOffsets.size() > 0) {
      ByteArrayHandle v = new ByteArrayHandle();
      in.seek(jpegOffsets.get(0));
      byte[] b = new byte[(int) (in.length() - in.getFilePointer())];
      in.read(b);
      try (RandomAccessInputStream s = new RandomAccessInputStream(b)) {
        for (long jpegOffset : jpegOffsets) {
          s.seek(jpegOffset - jpegOffsets.get(0));

          CodecOptions options = new CodecOptions();
          options.interleaved = isInterleaved();
          options.littleEndian = isLittleEndian();

          v.write(new JPEGCodec().decompress(s, options));
          }
      }

      try (RandomAccessInputStream s = new RandomAccessInputStream(v)) {
        s.seek(0);
        readPlane(s, x, y, w, h, buf);
      }

      return buf;
    }

    // combine everything in the strips Vector

    if ((getSizeY()*4 < strips.size()) && (((strips.size() / 3) %
      getSizeY()) != 0))
    {
      core.get(0).sizeY = strips.size();
    }

    int plane = w * h;

    if (lookup != null) {
      // 8 bit data

      byte[] row;

      for (int i=y; i<y+h; i++) {
        row = (byte[]) strips.get(i);
        int len = (int) Math.min(row.length, w);
        System.arraycopy(row, x, buf, (i - y) * w, len);
      }
    }
    else if (getSizeY()*3 == strips.size() || getSizeY()*4 == strips.size()) {
      // 24 or 32 bit data

      int nc = strips.size() / getSizeY();

      byte[] c0 = null;
      byte[] c1 = null;
      byte[] c2 = null;

      for (int i=y; i<h + y; i++) {
        c0 = (byte[]) strips.get(i * nc + nc - 3);
        c1 = (byte[]) strips.get(i * nc + nc - 2);
        c2 = (byte[]) strips.get(i * nc + nc - 1);
        int baseOffset = (i - y) * w;
        System.arraycopy(c0, x, buf, baseOffset, w);
        System.arraycopy(c1, x, buf, plane + baseOffset, w);
        System.arraycopy(c2, x, buf, 2*plane + baseOffset, w);
      }
    }
    else {
      // RGB value is packed into a single short: xRRR RRGG GGGB BBBB
      int[] row = null;
      for (int i=y; i<h + y; i++) {
        row = (int[]) strips.get(i);

        for (int j=x; j<w + x; j++) {
          int base = (i - y) * w + (j - x);
          buf[base] = (byte) ((row[j] & 0x7c00) >> 10);
          buf[plane + base] = (byte) ((row[j] & 0x3e0) >> 5);
          buf[2 * plane + base] = (byte) (row[j] & 0x1f);
        }
      }
    }
    return buf;
  }

  /* @see loci.formats.IFormatReader#close(boolean) */
  @Override
  public void close(boolean fileOnly) throws IOException {
    super.close(fileOnly);
    if (!fileOnly) {
      rowBytes = 0;
      strips = null;
      versionOne = false;
      lookup = null;
      jpegOffsets.clear();
    }
  }

  // -- Internal FormatReader API methods --

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

    CoreMetadata m = core.get(0);

    m.littleEndian = false;

    in.seek(518);

    m.sizeY = in.readShort();
    m.sizeX = in.readShort();
    m.sizeZ = 1;
    m.sizeC = 1;
    m.sizeT = 1;
    m.dimensionOrder = "XYCZT";
    m.imageCount = 1;
    m.falseColor = false;
    m.metadataComplete = true;
    m.interleaved = false;
    m.pixelType = FormatTools.UINT8;

    strips = new Vector();
    rowBytes = 0;
    lookup = null;

    int opcode;

    int verOpcode = in.read();
    int verNumber = in.read();

    if (verOpcode == 0x11 && verNumber == 0x01) versionOne = true;
    else if (verOpcode == 0x00 && verNumber == 0x11) {
      versionOne = false;
      int verNumber2 = in.readShort();

      if (verNumber2 != 0x02ff) {
        throw new FormatException("Invalid PICT file : " + verNumber2);
      }

      // skip over v2 header -- don't need it here
      //in.skipBytes(26);
      in.skipBytes(6);
      int pixelsPerInchX = in.readInt();
      int pixelsPerInchY = in.readInt();
      in.skipBytes(4);
      int y = in.readShort();
      int x = in.readShort();
      if (y > 0) m.sizeY = y;
      if (x > 0) m.sizeX = x;
      in.skipBytes(4);
    }
    else throw new FormatException("Invalid PICT file");

    addGlobalMeta("Version", versionOne ? 1 : 2);

    do {
      if (versionOne) opcode = in.read();
      else {
        // if at odd boundary skip a byte for opcode in PICT v2

        if ((in.getFilePointer() & 0x1L) != 0) {
          in.skipBytes(1);
        }
        if (in.getFilePointer() + 2 >= in.length()) {
          break;
        }
        opcode = in.readShort() & 0xffff;
      }
    }
    while (drivePictDecoder(opcode));

    m.rgb = getSizeC() > 1;
    m.indexed = !isRGB() && lookup != null;

    // The metadata store we're working with.
    MetadataStore store = makeFilterMetadata();
    MetadataTools.populatePixels(store, this);
  }

  // -- Helper methods --

  /** Handles the opcodes in the PICT file. */
  private boolean drivePictDecoder(int opcode)
    throws FormatException, IOException
  {
    LOGGER.debug("drivePictDecoder({}) @ {}", opcode, in.getFilePointer());

    switch (opcode) {
      case PICT_BITSRGN:  // rowBytes must be < 8
      case PICT_PACKBITSRGN: // rowBytes must be < 8
      case PICT_BITSRECT: // rowBytes must be < 8
      case PICT_PACKBITSRECT:
        rowBytes = in.readShort();
        if (versionOne || (rowBytes & 0x8000) == 0) handleBitmap(opcode);
        else handlePixmap(opcode);
        break;
      case PICT_9A:
        handlePixmap(opcode);
        break;
      case PICT_CLIP_RGN:
        int x = in.readShort();
        in.skipBytes(x - 2);
        break;
      case PICT_LONGCOMMENT:
        in.skipBytes(2);
        x = in.readShort();
        in.skipBytes(x);
        break;
      case PICT_END: // end of PICT
        return false;
      case PICT_TYPE_1:
      case PICT_TYPE_2:
        x = in.read();
        in.skipBytes(x);
        break;
      case PICT_JPEG:
        jpegOffsets.add(in.getFilePointer() + 2);
        core.get(0).sizeC = 3;
        core.get(0).rgb = true;
        while ((in.readShort() & 0xffff) != 0xffd9 &&
          in.getFilePointer() < in.length());
        while (in.getFilePointer() < in.length()) {
          while ((in.readShort() & 0xffff) != 0xffd8 &&
            in.getFilePointer() < in.length());
          if (in.getFilePointer() < in.length()) {
            jpegOffsets.add(in.getFilePointer() - 2);
          }
        }
        core.get(0).interleaved = true;
        break;
      default:
        if (opcode < 0) {
          //throw new FormatException("Invalid opcode: " + opcode);
          LOGGER.warn("Invalid opcode: {}", opcode);
        }
    }

    return in.getFilePointer() < in.length();
  }

  private void readImageHeader(int opcode) throws IOException {
    if (opcode == PICT_9A) in.skipBytes(6);
    else rowBytes &= 0x3fff;

    int tlY = in.readShort();
    int tlX = in.readShort();
    int brY = in.readShort();
    int brX = in.readShort();

    if (brX - tlX > 0) core.get(0).sizeX = brX - tlX;
    if (brY - tlY > 0) core.get(0).sizeY = brY - tlY;

    in.skipBytes(18);
  }

  /** Extract the image data in a PICT bitmap structure. */
  private void handleBitmap(int opcode) throws FormatException, IOException {
    readImageHeader(opcode);
    handlePixmap(1, 1);
  }

  /** Extracts the image data in a PICT pixmap structure. */
  private void handlePixmap(int opcode) throws FormatException, IOException {
    readImageHeader(opcode);
    LOGGER.debug("handlePixmap({})", opcode);

    int pixelSize = in.readShort();
    int compCount = in.readShort();
    in.skipBytes(14);

    if (opcode == PICT_9A) {
      // rowBytes doesn't exist, so set it to its logical value
      switch (pixelSize) {
        case 32:
          rowBytes = getSizeX() * compCount;
          break;
        case 16:
          rowBytes = getSizeX() * 2;
          break;
        default:
          throw new FormatException("Sorry, vector data not supported.");
      }
    }
    else {
      // read the lookup table

      in.skipBytes(4);
      int flags = in.readShort();
      int count = in.readShort();

      count++;
      lookup = new byte[3][count];

      for (int i=0; i<count; i++) {
        in.skipBytes(2);
        lookup[0][i] = in.readByte();
        in.skipBytes(1);
        lookup[1][i] = in.readByte();
        in.skipBytes(1);
        lookup[2][i] = in.readByte();
        in.skipBytes(1);
      }
    }

    // skip over two rectangles
    in.skipBytes(18);

    if (opcode == PICT_BITSRGN || opcode == PICT_PACKBITSRGN) in.skipBytes(2);

    handlePixmap(pixelSize, compCount);
  }

  /** Handles the unpacking of the image data. */
  private void handlePixmap(int pixelSize, int compCount)
    throws FormatException, IOException
  {
    LOGGER.debug("handlePixmap({}, {}, {})",
      new Object[] {rowBytes, pixelSize, compCount});
    int rawLen;
    byte[] buf;  // row raw bytes
    byte[] uBuf = null;  // row uncompressed data
    int[] uBufI = null;  // row uncompressed data - 16+ bit pixels
    int bufSize = rowBytes;
    int outBufSize = getSizeX();
    byte[] outBuf = null;  // used to expand pixel data

    boolean compressed = (rowBytes >= 8) || (pixelSize == 32);

    // allocate buffers

    switch (pixelSize) {
      case 32:
        if (!compressed) uBufI = new int[getSizeX()];
        else uBuf = new byte[bufSize];
        break;
      case 16:
        uBufI = new int[getSizeX()];
        break;
      case 8:
        uBuf = new byte[bufSize];
        break;
      default:
        outBuf = new byte[outBufSize];
        uBuf = new byte[bufSize];
        break;
    }

    if (!compressed) {
      LOGGER.debug("Pixel data is uncompressed (pixelSize={}).", pixelSize);
      buf = new byte[bufSize];
      for (int row=0; row<getSizeY(); row++) {
        in.read(buf, 0, rowBytes);

        switch (pixelSize) {
          case 16:
            for (int i=0; i<getSizeX(); i++) {
              uBufI[i] = DataTools.bytesToShort(buf, i*2, 2, false);
            }
            strips.add(uBufI);
            buf = null;
            core.get(0).sizeC = 3;
            break;
          case 8:
            strips.add(buf);
            break;
          default: // pixel size < 8
            expandPixels(pixelSize, buf, outBuf, outBuf.length);
            strips.add(outBuf);
            buf = null;
        }
      }
    }
    else {
      LOGGER.debug("Pixel data is compressed (pixelSize={}; compCount={}).",
        pixelSize, compCount);
      buf = new byte[bufSize + 1 + bufSize / 128];
      for (int row=0; row<getSizeY(); row++) {
        if (rowBytes > 250) rawLen = in.readShort();
        else rawLen = in.read();

        if (rawLen > buf.length) rawLen = buf.length;

        if ((in.length() - in.getFilePointer()) <= rawLen) {
          rawLen = (int) (in.length() - in.getFilePointer() - 1);
        }

        if (rawLen < 0) {
          rawLen = 0;
          in.seek(in.length() - 1);
        }

        in.read(buf, 0, rawLen);

        if (pixelSize == 16) {
          uBufI = new int[getSizeX()];
          unpackBits(buf, uBufI);
          strips.add(uBufI);
          core.get(0).sizeC = 3;
        }
        else {
          PackbitsCodec c = new PackbitsCodec();
          CodecOptions options = new CodecOptions();
          options.maxBytes = getSizeX() * 4;
          uBuf = c.decompress(buf, options);
        }

        if (pixelSize < 8) {
          expandPixels(pixelSize, uBuf, outBuf, outBuf.length);
          strips.add(outBuf);
        }
        else if (pixelSize == 8) {
          strips.add(uBuf);
        }
        else if (pixelSize == 24 || pixelSize == 32) {
          byte[] newBuf = null;

          for (int q=0; q<compCount; q++) {
            int offset = q * getSizeX();
            int len = (int) Math.min(getSizeX(), uBuf.length - offset);
            newBuf = new byte[getSizeX()];
            if (offset < uBuf.length) {
              System.arraycopy(uBuf, offset, newBuf, 0, len);
            }
            strips.add(newBuf);
          }
          core.get(0).sizeC = 3;
        }
      }
    }
  }

  /** Expand an array of bytes. */
  private void expandPixels(int bitSize, byte[] ib, byte[] ob, int outLen)
    throws FormatException
  {
    LOGGER.debug("expandPixels({}, {}, {}, {})",
      new Object[] {bitSize, ib.length, ob.length, outLen});
    if (bitSize == 1) {
      int remainder = outLen % 8;
      int max = outLen / 8;
      for (int i=0; i<max; i++) {
        if (i < ib.length) {
          int look = (ib[i] & 0xff) * 8;
          System.arraycopy(EXPANSION_TABLE, look, ob, i*8, 8);
        }
        else i = max;
      }

      if (remainder != 0) {
        if (max < ib.length) {
          System.arraycopy(EXPANSION_TABLE, (ib[max] & 0xff) * 8, ob,
            max*8, remainder);
        }
      }

      return;
    }

    byte v;
    int count = 8 / bitSize; // number of pixels in a byte
    int maskshift = bitSize; // num bits to shift mask
    int pixelshift = 8 - bitSize; // num bits to shift pixel
    int tpixelshift = 0;
    int pixelshiftdelta = bitSize;
    int tmask; // temp mask

    if (bitSize != 1 && bitSize != 2 && bitSize != 4) {
      throw new FormatException("Can only expand 1, 2, and 4 bit values");
    }

    int mask = ((int) Math.pow(2, bitSize) - 1) << (8 - bitSize);

    int i = 0;
    for (int o = 0; o < ob.length; i++) {
      tmask = mask;
      tpixelshift = pixelshift;
      v = ib[i];
      for (int t = 0; t < count && o < ob.length; t++, o++) {
        ob[o] = (byte) (((v & tmask) >>> tpixelshift) & 0xff);
        tmask = (byte) ((tmask & 0xff) >>> maskshift);
        tpixelshift -= pixelshiftdelta;
      }
    }
  }

  /** PackBits variant that outputs an int array. */
  private void unpackBits(byte[] ib, int[] ob) {
    LOGGER.debug("unpackBits(...)");
    int i = 0;
    int b;
    int rep;
    int end;

    for (int o=0; o<ob.length;) {
      if (i+1 < ib.length) {
        b = ib[i++];
        if (b >= 0) {
          end = o + b + 1;
          while (o < end && o < ob.length && (i + 1) < ib.length) {
            ob[o++] = DataTools.bytesToShort(ib, i, 2, false);
            i += 2;
          }
        }
        else if (b != -128) {
          rep = DataTools.bytesToShort(ib, i, 2, false);
          i += 2;
          end = o - b + 1;
          while (o < end && o < ob.length) {
            ob[o++] = rep;
          }
        }
      }
      else o = ob.length;
    }
  }

}
