/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id: PNGFile.java 1732019 2016-02-24 05:01:10Z gadams $ */

package org.apache.xmlgraphics.image.loader.impl;

import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;

import org.apache.xmlgraphics.image.codec.png.PNGChunk;
import org.apache.xmlgraphics.image.codec.util.PropertyUtil;
import org.apache.xmlgraphics.image.loader.ImageException;
import org.apache.xmlgraphics.image.loader.ImageInfo;

// CSOFF: MethodName

/**
 * Provides methods useful for processing PNG files.
 */
class PNGFile implements PNGConstants {

    private ColorModel colorModel;
    private ICC_Profile iccProfile;
    private int sRGBRenderingIntent = -1;
    private int bitDepth;
    private int colorType;
    private boolean isTransparent;
    private int grayTransparentAlpha;
    private int redTransparentAlpha;
    private int greenTransparentAlpha;
    private int blueTransparentAlpha;
    private List<InputStream> streamVec = new ArrayList<InputStream>();
    private int paletteEntries;
    private byte[] redPalette;
    private byte[] greenPalette;
    private byte[] bluePalette;
    private byte[] alphaPalette;
    private boolean hasPalette;
    private boolean hasAlphaPalette;

    public PNGFile(InputStream stream) throws IOException, ImageException {
        if (!stream.markSupported()) {
            stream = new BufferedInputStream(stream);
        }
        DataInputStream distream = new DataInputStream(stream);
        long magic = distream.readLong();
        if (magic != PNG_SIGNATURE) {
            String msg = PropertyUtil.getString("PNGImageDecoder0");
            throw new ImageException(msg);
        }
        // only some chunks are worth parsing in the current implementation
        do {
            try {
                PNGChunk chunk;
                String chunkType = PNGChunk.getChunkType(distream);
                if (chunkType.equals(PNGChunk.ChunkType.IHDR.name())) {
                    chunk = PNGChunk.readChunk(distream);
                    parse_IHDR_chunk(chunk);
                } else if (chunkType.equals(PNGChunk.ChunkType.PLTE.name())) {
                    chunk = PNGChunk.readChunk(distream);
                    parse_PLTE_chunk(chunk);
                } else if (chunkType.equals(PNGChunk.ChunkType.IDAT.name())) {
                    chunk = PNGChunk.readChunk(distream);
                    streamVec.add(new ByteArrayInputStream(chunk.getData()));
                } else if (chunkType.equals(PNGChunk.ChunkType.IEND.name())) {
                    // chunk = PNGChunk.readChunk(distream);
                    PNGChunk.skipChunk(distream);
                    break; // fall through to the bottom
                } else if (chunkType.equals(PNGChunk.ChunkType.tRNS.name())) {
                    chunk = PNGChunk.readChunk(distream);
                    parse_tRNS_chunk(chunk);
                } else if (chunkType.equals(PNGChunk.ChunkType.iCCP.name())) {
                    chunk = PNGChunk.readChunk(distream);
                    parse_iCCP_chunk(chunk);
                } else if (chunkType.equals(PNGChunk.ChunkType.sRGB.name())) {
                  chunk = PNGChunk.readChunk(distream);
                  parse_sRGB_chunk(chunk);
                } else {
                    // chunk = PNGChunk.readChunk(distream);
                    PNGChunk.skipChunk(distream);
                }
            } catch (Exception e) {
                String msg = PropertyUtil.getString("PNGImageDecoder2");
                throw new RuntimeException(msg, e);
            }
        } while (true);
    }

    public ImageRawPNG getImageRawPNG(ImageInfo info) throws ImageException {
        InputStream seqStream = new SequenceInputStream(Collections.enumeration(streamVec));
        ColorSpace rgbCS = null;
        switch (colorType) {
        case PNG_COLOR_GRAY:
            if (hasPalette) {
                throw new ImageException("Corrupt PNG: color palette is not allowed!");
            }
            colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false,
                    ColorModel.OPAQUE, DataBuffer.TYPE_BYTE);
            break;
        case PNG_COLOR_RGB:
            if (iccProfile != null) {
                rgbCS = new ICC_ColorSpace(iccProfile);
            } else if (sRGBRenderingIntent != -1) {
                rgbCS = ColorSpace.getInstance(ColorSpace.CS_sRGB);
            } else {
                rgbCS = ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB);
            }
            colorModel = new ComponentColorModel(rgbCS, false, false, ColorModel.OPAQUE, DataBuffer.TYPE_BYTE);
            break;
        case PNG_COLOR_PALETTE:
            if (hasAlphaPalette) {
                colorModel = new IndexColorModel(bitDepth, paletteEntries, redPalette, greenPalette,
                        bluePalette, alphaPalette);
            } else {
                colorModel = new IndexColorModel(bitDepth, paletteEntries, redPalette, greenPalette,
                        bluePalette);
            }
            break;
        case PNG_COLOR_GRAY_ALPHA:
            if (hasPalette) {
                throw new ImageException("Corrupt PNG: color palette is not allowed!");
            }
            colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), true, false,
                    ColorModel.TRANSLUCENT, DataBuffer.TYPE_BYTE);
            break;
        case PNG_COLOR_RGB_ALPHA:
            if (iccProfile != null) {
                rgbCS = new ICC_ColorSpace(iccProfile);
            } else if (sRGBRenderingIntent != -1) {
                rgbCS = ColorSpace.getInstance(ColorSpace.CS_sRGB);
            } else {
                rgbCS = ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB);
            }
            colorModel = new ComponentColorModel(rgbCS, true, false, ColorModel.TRANSLUCENT,
                    DataBuffer.TYPE_BYTE);
            break;
        default:
            throw new ImageException("Unsupported color type: " + colorType);
        }
        // the iccProfile is still null for now
        ImageRawPNG rawImage = new ImageRawPNG(info, seqStream, colorModel, bitDepth, iccProfile);
        if (isTransparent) {
            if (colorType == PNG_COLOR_GRAY) {
                rawImage.setGrayTransparentAlpha(grayTransparentAlpha);
            } else if (colorType == PNG_COLOR_RGB) {
                rawImage.setRGBTransparentAlpha(redTransparentAlpha, greenTransparentAlpha,
                        blueTransparentAlpha);
            } else if (colorType == PNG_COLOR_PALETTE) {
                rawImage.setTransparent();
            } else {
                //
            }
        }
        if (sRGBRenderingIntent != -1) {
          rawImage.setRenderingIntent(sRGBRenderingIntent);
        }
        return rawImage;
    }

    private void parse_IHDR_chunk(PNGChunk chunk) {
        bitDepth = chunk.getInt1(8);
        if (bitDepth != 8) {
            // this is a limitation of the current implementation
            throw new RuntimeException("Unsupported bit depth: " + bitDepth);
        }
        colorType = chunk.getInt1(9);
        int compressionMethod = chunk.getInt1(10);
        if (compressionMethod != 0) {
            throw new RuntimeException("Unsupported PNG compression method: " + compressionMethod);
        }
        int filterMethod = chunk.getInt1(11);
        if (filterMethod != 0) {
            throw new RuntimeException("Unsupported PNG filter method: " + filterMethod);
        }
        int interlaceMethod = chunk.getInt1(12);
        if (interlaceMethod != 0) {
            // this is a limitation of the current implementation
            throw new RuntimeException("Unsupported PNG interlace method: " + interlaceMethod);
        }
    }

    private void parse_PLTE_chunk(PNGChunk chunk) {
        paletteEntries = chunk.getLength() / 3;
        redPalette = new byte[paletteEntries];
        greenPalette = new byte[paletteEntries];
        bluePalette = new byte[paletteEntries];
        hasPalette = true;

        int pltIndex = 0;
        for (int i = 0; i < paletteEntries; i++) {
            redPalette[i] = chunk.getByte(pltIndex++);
            greenPalette[i] = chunk.getByte(pltIndex++);
            bluePalette[i] = chunk.getByte(pltIndex++);
        }
    }

    private void parse_tRNS_chunk(PNGChunk chunk) {
        if (colorType == PNG_COLOR_PALETTE) {
            int entries = chunk.getLength();
            if (entries > paletteEntries) {
                // Error -- mustn't have more alpha than RGB palette entries
                String msg = PropertyUtil.getString("PNGImageDecoder14");
                throw new RuntimeException(msg);
            }
            // Load beginning of palette from the chunk
            alphaPalette = new byte[paletteEntries];
            for (int i = 0; i < entries; i++) {
                alphaPalette[i] = chunk.getByte(i);
            }
            // Fill rest of palette with 255
            for (int i = entries; i < paletteEntries; i++) {
                alphaPalette[i] = (byte) 255;
            }
            hasAlphaPalette = true;
        } else if (colorType == PNG_COLOR_GRAY) {
            grayTransparentAlpha = chunk.getInt2(0);
        } else if (colorType == PNG_COLOR_RGB) {
            redTransparentAlpha = chunk.getInt2(0);
            greenTransparentAlpha = chunk.getInt2(2);
            blueTransparentAlpha = chunk.getInt2(4);
        } else if (colorType == PNG_COLOR_GRAY_ALPHA || colorType == PNG_COLOR_RGB_ALPHA) {
            // Error -- GA or RGBA image can't have a tRNS chunk.
            String msg = PropertyUtil.getString("PNGImageDecoder15");
            throw new RuntimeException(msg);
        }
        isTransparent = true;
    }

    private void parse_iCCP_chunk(PNGChunk chunk) {
        int length = chunk.getLength();
        int textIndex = 0;
        while (chunk.getByte(textIndex++) != 0) {
            //NOP
        }
        textIndex++;
        byte[] profile = new byte[length - textIndex];
        System.arraycopy(chunk.getData(), textIndex, profile, 0, length - textIndex);
        ByteArrayInputStream bais = new ByteArrayInputStream(profile);
        InflaterInputStream iis = new InflaterInputStream(bais, new Inflater());
        try {
            iccProfile = ICC_Profile.getInstance(iis);
        } catch (IOException ioe) {
            // this is OK; the profile will be null
        }
    }

    private void parse_sRGB_chunk(PNGChunk chunk) {
      sRGBRenderingIntent = chunk.getByte(0);
    }

}
