/*
 * #%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.tiff;

import java.io.IOException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;

import loci.common.DataTools;
import loci.common.enumeration.CodedEnum;
import loci.common.enumeration.EnumException;
import loci.formats.FormatException;
import loci.formats.UnsupportedCompressionException;
import loci.formats.codec.Codec;
import loci.formats.codec.CodecOptions;
import loci.formats.codec.JPEG2000Codec;
import loci.formats.codec.JPEG2000CodecOptions;
import loci.formats.codec.JPEGCodec;
import loci.formats.codec.JPEGXRCodec;
import loci.formats.codec.LZWCodec;
import loci.formats.codec.NikonCodec;
import loci.formats.codec.PackbitsCodec;
import loci.formats.codec.PassthroughCodec;
import loci.formats.codec.ZlibCodec;
import loci.formats.codec.ZstdCodec;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility class for performing compression operations with a TIFF file.
 *
 * @author Curtis Rueden ctrueden at wisc.edu
 * @author Eric Kjellman egkjellman at wisc.edu
 * @author Melissa Linkert melissa at glencoesoftware.com
 * @author Chris Allan callan at blackcat.ca
 */
public enum TiffCompression implements CodedEnum {

  // (TIFF code, codec, codec name)
  DEFAULT_UNCOMPRESSED(0, new PassthroughCodec(), "Uncompressed"),
  UNCOMPRESSED(1, new PassthroughCodec(), "Uncompressed"),
  CCITT_1D(2, null, "CCITT Group 3 1-Dimensional Modified Huffman"),
  GROUP_3_FAX(3, null, "CCITT T.4 bi-level encoding (Group 3 Fax)"),
  GROUP_4_FAX(4, null, "CCITT T.6 bi-level encoding (Group 4 Fax)"),
  LZW(5, new LZWCodec(), "LZW"),
  OLD_JPEG(6, new JPEGCodec(), "Old JPEG"),
  JPEG(7, new JPEGCodec(), "JPEG"),
  PACK_BITS(32773, new PackbitsCodec(), "PackBits"),
  PROPRIETARY_DEFLATE(32946, new ZlibCodec(), "Deflate (Zlib)"),
  DEFLATE(8, new ZlibCodec(), "Deflate (Zlib)"),
  THUNDERSCAN(32809, null, "Thunderscan"),
  JPEG_2000(33003, new JPEG2000Codec(), "JPEG-2000") {
    @Override
    public CodecOptions getCompressionCodecOptions(IFD ifd)
        throws FormatException {
      return getCompressionCodecOptions(ifd, null);
    }

    @Override
    public CodecOptions getCompressionCodecOptions(IFD ifd, CodecOptions opt)
    throws FormatException {
      CodecOptions options = super.getCompressionCodecOptions(ifd, opt);
      JPEG2000CodecOptions j2k = JPEG2000CodecOptions.getDefaultOptions(options);
      if (opt instanceof JPEG2000CodecOptions) {
        JPEG2000CodecOptions o = (JPEG2000CodecOptions) opt;
        j2k.numDecompositionLevels = o.numDecompositionLevels;
        j2k.resolution = o.resolution;
        if (o.codeBlockSize != null) {
          j2k.codeBlockSize = o.codeBlockSize;
        }
        if (o.quality > 0) {
          j2k.quality = o.quality;
        }
      }
      j2k.writeBox = false;
      j2k.lossless = false;
      return j2k;
    }
  },
  JPEG_2000_LOSSY(33004, new JPEG2000Codec(), "JPEG-2000 Lossy") {
    @Override
    public CodecOptions getCompressionCodecOptions(IFD ifd)
        throws FormatException {
      return getCompressionCodecOptions(ifd, null);
    }

    @Override
    public CodecOptions getCompressionCodecOptions(IFD ifd, CodecOptions opt)
    throws FormatException {
      CodecOptions options = super.getCompressionCodecOptions(ifd, opt);
      options.lossless = false;
      JPEG2000CodecOptions j2k = JPEG2000CodecOptions.getDefaultOptions(options);
      if (opt instanceof JPEG2000CodecOptions) {
        JPEG2000CodecOptions o = (JPEG2000CodecOptions) opt;
        j2k.numDecompositionLevels = o.numDecompositionLevels;
        j2k.resolution = o.resolution;
        if (o.codeBlockSize != null) {
          j2k.codeBlockSize = o.codeBlockSize;
        }
        if (o.quality > 0) {
          j2k.quality = o.quality;
        }
      }
      j2k.writeBox = false;
      return j2k;
    }
  },
  ALT_JPEG2000(33005, new JPEG2000Codec(), "JPEG-2000") {
    @Override
    public CodecOptions getCompressionCodecOptions(IFD ifd)
        throws FormatException
    {
      return getCompressionCodecOptions(ifd, null);
    }
    
    @Override
    public CodecOptions getCompressionCodecOptions(IFD ifd, CodecOptions opt)
    throws FormatException {
      CodecOptions options = super.getCompressionCodecOptions(ifd, opt);
      options.lossless = true;
      JPEG2000CodecOptions j2k = JPEG2000CodecOptions.getDefaultOptions(options);
      if (opt instanceof JPEG2000CodecOptions) {
        JPEG2000CodecOptions o = (JPEG2000CodecOptions) opt;
        j2k.numDecompositionLevels = o.numDecompositionLevels;
        j2k.resolution = o.resolution;
        if (o.codeBlockSize != null)
          j2k.codeBlockSize = o.codeBlockSize;
        if (o.quality > 0)
          j2k.quality = o.quality;
      }
      j2k.writeBox = false;
      j2k.lossless = false;
      return j2k;
    }
  },
  ALT_JPEG(33007, new JPEGCodec(), "JPEG"),
  OLYMPUS_JPEG2000(34712, new JPEG2000Codec(), "JPEG-2000") {
    @Override
    public CodecOptions getCompressionCodecOptions(IFD ifd)
        throws FormatException
    {
      return getCompressionCodecOptions(ifd, null);
    }
    
    @Override
    public CodecOptions getCompressionCodecOptions(IFD ifd, CodecOptions opt)
    throws FormatException {
      CodecOptions options = super.getCompressionCodecOptions(ifd, opt);
      options.lossless = true;
      JPEG2000CodecOptions j2k = JPEG2000CodecOptions.getDefaultOptions(options);
      if (opt instanceof JPEG2000CodecOptions) {
        JPEG2000CodecOptions o = (JPEG2000CodecOptions) opt;
        j2k.numDecompositionLevels = o.numDecompositionLevels;
        j2k.resolution = o.resolution;
        if (o.codeBlockSize != null)
          j2k.codeBlockSize = o.codeBlockSize;
        if (o.quality > 0)
          j2k.quality = o.quality;
      }
      return j2k;
    }
 
  },
  NIKON(34713, new NikonCodec(), "Nikon"),
  JPEGXR(22610, new JPEGXRCodec(), "JPEG-XR"),
  ZSTD(50000, new ZstdCodec(), "Zstandard");

  // -- Constants --

  private static final Logger LOGGER =
    LoggerFactory.getLogger(TiffCompression.class);

  /** Code for the TIFF compression in the actual TIFF file. */
  private int code;

  /** TIFF compression codec. */
  private Codec codec;

  /** Name of the TIFF compression codec. */
  private String codecName;

  /** Reverse lookup of code to TIFF compression enumerate value. */
  private static final Map<Integer, TiffCompression> lookup =
    getCompressionMap();

  private static Map<Integer, TiffCompression> getCompressionMap() {
    Map<Integer, TiffCompression> lookup =
      new HashMap<Integer, TiffCompression>();
    for (TiffCompression v : EnumSet.allOf(TiffCompression.class)) {
      lookup.put(v.getCode(), v);
    }
    return lookup;
  }

  // -- TiffCompression methods --

  /**
   * Default constructor.
   * @param code Integer "code" for the TIFF compression type.
   * @param codec TIFF compression codec.
   * @param codecName String name of the compression type.
   */
  private TiffCompression(int code, Codec codec, String codecName) {
    this.code = code;
    this.codec = codec;
    this.codecName = codecName;
  }

  /**
   * Retrieves a TIFF compression instance by code.
   * @param code Integer "code" for the TIFF compression type.
   * @return See above.
   */
  public static TiffCompression get(int code) {
    TiffCompression toReturn = lookup.get(code);
    if (toReturn == null) {
      throw new EnumException(
          "Unable to find TiffCompresssion with code: " + code);
    }
    return toReturn;
  }

  /* (non-Javadoc)
   * @see loci.common.CodedEnum#getCode()
   */
  @Override
  public int getCode() {
    return code;
  }

  /**
   * Retrieves the name of the TIFF compression codec.
   * @return See above.
   */
  public String getCodecName() {
    return codecName;
  }

  /**
   * @return the codec instance for this TIFF compression.
   */
  public Codec getCodec() {
    return codec;
  }

  // -- TiffCompression methods - decompression --

  /** Decodes a strip of data. */
  public byte[] decompress(byte[] input, CodecOptions options)
    throws FormatException, IOException
  {
    if (codec == null) {
      throw new UnsupportedCompressionException(
          "Sorry, " + getCodecName() + " compression mode is not supported");
    }
    return codec.decompress(input, options);
  }

  /** Undoes in-place differencing according to the given predictor value. */
  public static void undifference(byte[] input, IFD ifd)
    throws FormatException
  {
    int predictor = ifd.getIFDIntValue(IFD.PREDICTOR, 1);
    if (predictor == 2) {
      LOGGER.debug("reversing horizontal differencing");
      int[] bitsPerSample = ifd.getBitsPerSample();
      int len = bitsPerSample.length;
      long width = ifd.getTileWidth();
      boolean little = ifd.isLittleEndian();
      int planarConfig = ifd.getPlanarConfiguration();

      int bytes = ifd.getBytesPerSample()[0];

      if (planarConfig == 2 || bitsPerSample[len - 1] == 0) len = 1;
      len *= bytes;

      for (int b=0; b<=input.length-bytes; b+=bytes) {
        if (b / len % width == 0) continue;
        int value = DataTools.bytesToInt(input, b, bytes, little);
        value += DataTools.bytesToInt(input, b - len, bytes, little);
        DataTools.unpackBytes(value, input, b, bytes, little);
      }
    }
    else if (predictor != 1) {
      throw new FormatException("Unknown Predictor (" + predictor + ")");
    }
  }

  // -- TiffCompression methods - compression --

  /**
   * Creates a set of codec options for compression.
   * @param ifd The IFD to create codec options for.
   * @return A new codec options instance populated using metadata from
   * <code>ifd</code>.
   */
  public CodecOptions getCompressionCodecOptions(IFD ifd)
    throws FormatException{
    return getCompressionCodecOptions(ifd, null);
  }

  /**
   * Creates a set of codec options for compression.
   * @param ifd The IFD to create codec options for.
   * @return A new codec options instance populated using metadata from
   * <code>ifd</code>.
   * @param opt The codec options to copy.
   */
  public CodecOptions getCompressionCodecOptions(IFD ifd, CodecOptions opt)
    throws FormatException{
    if (ifd == null)
      throw new IllegalArgumentException("No IFD specified.");
    if (opt == null) opt = CodecOptions.getDefaultOptions();
    CodecOptions options = new CodecOptions(opt);
    options.width = (int) ifd.getImageWidth();
    options.height = (int) ifd.getImageLength();
    options.bitsPerSample = ifd.getBitsPerSample()[0];
    options.channels = ifd.getSamplesPerPixel();
    options.littleEndian = ifd.isLittleEndian();
    options.interleaved = true;
    options.signed = false;
    return options;
  }
  
  /** Encodes a strip of data. */
  public byte[] compress(byte[] input, CodecOptions options)
    throws FormatException, IOException
  {
    if (codec == null) {
      throw new FormatException(
          "Sorry, " + getCodecName() + " compression mode is not supported");
    }
    return codec.compress(input, options);
  }

  /** Performs in-place differencing according to the given predictor value. */
  public static void difference(byte[] input, IFD ifd) throws FormatException {
    int predictor = ifd.getIFDIntValue(IFD.PREDICTOR, 1);
    if (predictor == 2) {
      LOGGER.debug("performing horizontal differencing");
      int[] bitsPerSample = ifd.getBitsPerSample();
      long width = ifd.getTileWidth();
      boolean little = ifd.isLittleEndian();
      int planarConfig = ifd.getPlanarConfiguration();
      int bytes = ifd.getBytesPerSample()[0];
      int len = bytes * (planarConfig == 2 ? 1 : bitsPerSample.length);

      for (int b=input.length-bytes; b>=0; b-=bytes) {
        if (b / len % width == 0) continue;
        int value = DataTools.bytesToInt(input, b, bytes, little);
        value -= DataTools.bytesToInt(input, b - len, bytes, little);
        DataTools.unpackBytes(value, input, b, bytes, little);
      }
    }
    else if (predictor != 1) {
      throw new FormatException("Unknown Predictor (" + predictor + ")");
    }
  }

}
