/**
 * 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.
 */

package org.apache.pdfbox.jbig2.segments;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;

import org.apache.pdfbox.jbig2.Bitmap;
import org.apache.pdfbox.jbig2.Region;
import org.apache.pdfbox.jbig2.SegmentHeader;
import org.apache.pdfbox.jbig2.err.InvalidHeaderValueException;
import org.apache.pdfbox.jbig2.image.Bitmaps;
import org.apache.pdfbox.jbig2.io.SubInputStream;
import org.apache.pdfbox.jbig2.util.CombinationOperator;
import org.apache.pdfbox.jbig2.util.log.Logger;
import org.apache.pdfbox.jbig2.util.log.LoggerFactory;

/**
 * This class represents the data of segment type "Halftone region". Parsing is described in 7.4.5, page 67. Decoding
 * procedure in 6.6.5 and 7.4.5.2.
 */
public class HalftoneRegion implements Region
{

    private final Logger log = LoggerFactory.getLogger(HalftoneRegion.class);

    private SubInputStream subInputStream;
    private SegmentHeader segmentHeader;
    private long dataHeaderOffset;
    private long dataHeaderLength;
    private long dataOffset;
    private long dataLength;

    /** Region segment information field, 7.4.1 */
    private RegionSegmentInformation regionInfo;

    /** Halftone segment information field, 7.4.5.1.1 */
    private byte hDefaultPixel;
    private CombinationOperator hCombinationOperator;
    private boolean hSkipEnabled;
    private byte hTemplate;
    private boolean isMMREncoded;

    /** Halftone grid position and size, 7.4.5.1.2 */
    /** Width of the gray-scale image, 7.4.5.1.2.1 */
    private int hGridWidth;
    /** Height of the gray-scale image, 7.4.5.1.2.2 */
    private int hGridHeight;
    /** Horizontal offset of the grid, 7.4.5.1.2.3 */
    private int hGridX;
    /** Vertical offset of the grid, 7.4.5.1.2.4 */
    private int hGridY;

    /** Halftone grid vector, 7.4.5.1.3 */
    /** Horizontal coordinate of the halftone grid vector, 7.4.5.1.3.1 */
    private int hRegionX;
    /** Vertical coordinate of the halftone grod vector, 7.4.5.1.3.2 */
    private int hRegionY;

    /** Decoded data */
    private Bitmap halftoneRegionBitmap;

    /**
     * Previously decoded data from other regions or dictionaries, stored to use as patterns in this region.
     */
    private ArrayList<Bitmap> patterns;

    public HalftoneRegion()
    {
    }

    public HalftoneRegion(final SubInputStream subInputStream)
    {
        this.subInputStream = subInputStream;
        this.regionInfo = new RegionSegmentInformation(subInputStream);
    }

    public HalftoneRegion(final SubInputStream subInputStream, final SegmentHeader segmentHeader)
    {
        this.subInputStream = subInputStream;
        this.segmentHeader = segmentHeader;
        this.regionInfo = new RegionSegmentInformation(subInputStream);
    }

    private void parseHeader() throws IOException, InvalidHeaderValueException
    {
        regionInfo.parseHeader();

        /* Bit 7 */
        hDefaultPixel = (byte) subInputStream.readBit();

        /* Bit 4-6 */
        hCombinationOperator = CombinationOperator
                .translateOperatorCodeToEnum((short) (subInputStream.readBits(3) & 0xf));

        /* Bit 3 */
        if (subInputStream.readBit() == 1)
        {
            hSkipEnabled = true;
        }

        /* Bit 1-2 */
        hTemplate = (byte) (subInputStream.readBits(2) & 0xf);

        /* Bit 0 */
        if (subInputStream.readBit() == 1)
        {
            isMMREncoded = true;
        }

        hGridWidth = (int) (subInputStream.readBits(32) & 0xffffffff);
        hGridHeight = (int) (subInputStream.readBits(32) & 0xffffffff);

        hGridX = (int) subInputStream.readBits(32);
        hGridY = (int) subInputStream.readBits(32);

        hRegionX = (int) subInputStream.readBits(16) & 0xffff;
        hRegionY = (int) subInputStream.readBits(16) & 0xffff;

        /* Segment data structure */
        computeSegmentDataStructure();

        this.checkInput();
    }

    private void computeSegmentDataStructure() throws IOException
    {
        dataOffset = subInputStream.getStreamPosition();
        dataHeaderLength = dataOffset - dataHeaderOffset;
        dataLength = subInputStream.length() - dataHeaderLength;
    }

    private void checkInput() throws InvalidHeaderValueException
    {
        if (isMMREncoded)
        {
            if (hTemplate != 0)
            {
                log.info("hTemplate = " + hTemplate + " (should contain the value 0)");
            }

            if (hSkipEnabled)
            {
                log.info("hSkipEnabled 0 " + hSkipEnabled + " (should contain the value false)");
            }
        }
    }

    /**
     * The procedure is described in JBIG2 ISO standard, 6.6.5.
     * 
     * @return The decoded {@link Bitmap} of this region.
     * 
     * @throws IOException if an underlying IO operation fails
     * @throws InvalidHeaderValueException if a segment header value is invalid
     */
    public Bitmap getRegionBitmap() throws IOException, InvalidHeaderValueException
    {
        if (null == halftoneRegionBitmap)
        {

            /* 6.6.5, page 40 */
            /* 1) */
            halftoneRegionBitmap = new Bitmap(regionInfo.getBitmapWidth(),
                    regionInfo.getBitmapHeight());

            if (patterns == null)
            {
                patterns = getPatterns();
            }

            if (hDefaultPixel == 1)
            {
                Arrays.fill(halftoneRegionBitmap.getByteArray(), (byte) 0xff);
            }

            /* 2) */
            /*
             * 6.6.5.1 Computing hSkip - At the moment SKIP is not used... we are not able to test it.
             */
            // Bitmap hSkip;
            // if (hSkipEnabled) {
            // int hPatternHeight = (int) hPats.get(0).getHeight();
            // int hPatternWidth = (int) hPats.get(0).getWidth();
            // TODO: Set or get pattern width and height from referred
            // pattern segments. The method is called like this:
            // hSkip = computeHSkip(hPatternHeight, hPatternWidth);
            // }

            /* 3) */
            final int bitsPerValue = (int) Math.ceil(Math.log(patterns.size()) / Math.log(2));

            /* 4) */
            final int[][] grayScaleValues = grayScaleDecoding(bitsPerValue);

            /* 5), rendering the pattern, described in 6.6.5.2 */
            renderPattern(grayScaleValues);
        }
        /* 6) */
        return halftoneRegionBitmap;
    }

    /**
     * This method draws the pattern into the region bitmap ({@code htReg}), as described in 6.6.5.2, page 42
     */
    private void renderPattern(final int[][] grayScaleValues)
    {
        int x = 0, y = 0;

        // 1)
        for (int m = 0; m < hGridHeight; m++)
        {
            // a)
            for (int n = 0; n < hGridWidth; n++)
            {
                // i)
                x = computeX(m, n);
                y = computeY(m, n);

                // ii)
                final Bitmap patternBitmap = patterns.get(grayScaleValues[m][n]);
                Bitmaps.blit(patternBitmap, halftoneRegionBitmap, (x + hGridX), (y + hGridY),
                        hCombinationOperator);
            }
        }
    }

    /**
     * @throws IOException
     * @throws InvalidHeaderValueException
     * 
     */
    private ArrayList<Bitmap> getPatterns() throws InvalidHeaderValueException, IOException
    {
        final ArrayList<Bitmap> patterns = new ArrayList<Bitmap>();

        for (SegmentHeader s : segmentHeader.getRtSegments())
        {
            final PatternDictionary patternDictionary = (PatternDictionary) s.getSegmentData();
            patterns.addAll(patternDictionary.getDictionary());
        }

        return patterns;
    }

    /**
     * Gray-scale image decoding procedure is special for halftone region decoding and is described in Annex C.5 on page
     * 98.
     */
    private int[][] grayScaleDecoding(final int bitsPerValue) throws IOException
    {

        short[] gbAtX = null;
        short[] gbAtY = null;

        if (!isMMREncoded)
        {
            gbAtX = new short[4];
            gbAtY = new short[4];
            // Set AT pixel values
            if (hTemplate <= 1)
                gbAtX[0] = 3;
            else if (hTemplate >= 2)
                gbAtX[0] = 2;

            gbAtY[0] = -1;
            gbAtX[1] = -3;
            gbAtY[1] = -1;
            gbAtX[2] = 2;
            gbAtY[2] = -2;
            gbAtX[3] = -2;
            gbAtY[3] = -2;
        }

        Bitmap[] grayScalePlanes = new Bitmap[bitsPerValue];

        // 1)
        GenericRegion genericRegion = new GenericRegion(subInputStream);
        genericRegion.setParameters(isMMREncoded, dataOffset, dataLength, hGridHeight, hGridWidth,
                hTemplate, false, hSkipEnabled, gbAtX, gbAtY);

        // 2)
        int j = bitsPerValue - 1;

        grayScalePlanes[j] = genericRegion.getRegionBitmap();

        while (j > 0)
        {
            j--;
            genericRegion.resetBitmap();
            // 3) a)
            grayScalePlanes[j] = genericRegion.getRegionBitmap();
            // 3) b)
            grayScalePlanes = combineGrayScalePlanes(grayScalePlanes, j);
        }

        // 4)
        return computeGrayScaleValues(grayScalePlanes, bitsPerValue);
    }

    private Bitmap[] combineGrayScalePlanes(Bitmap[] grayScalePlanes, int j)
    {
        int byteIndex = 0;
        for (int y = 0; y < grayScalePlanes[j].getHeight(); y++)
        {

            for (int x = 0; x < grayScalePlanes[j].getWidth(); x += 8)
            {
                final byte newValue = grayScalePlanes[j + 1].getByte(byteIndex);
                final byte oldValue = grayScalePlanes[j].getByte(byteIndex);

                grayScalePlanes[j].setByte(byteIndex++,
                        Bitmaps.combineBytes(oldValue, newValue, CombinationOperator.XOR));
            }
        }
        return grayScalePlanes;
    }

    private int[][] computeGrayScaleValues(final Bitmap[] grayScalePlanes, final int bitsPerValue)
    {
        // Gray-scale decoding procedure, page 98
        final int[][] grayScaleValues = new int[hGridHeight][hGridWidth];

        // 4)
        for (int y = 0; y < hGridHeight; y++)
        {
            for (int x = 0; x < hGridWidth; x += 8)
            {
                final int minorWidth = hGridWidth - x > 8 ? 8 : hGridWidth - x;
                int byteIndex = grayScalePlanes[0].getByteIndex(x, y);

                for (int minorX = 0; minorX < minorWidth; minorX++)
                {
                    final int i = minorX + x;
                    grayScaleValues[y][i] = 0;

                    for (int j = 0; j < bitsPerValue; j++)
                    {
                        grayScaleValues[y][i] += ((grayScalePlanes[j]
                                .getByte(byteIndex) >> (7 - i & 7)) & 1) * (1 << j);
                    }
                }
            }
        }
        return grayScaleValues;
    }

    private int computeX(final int m, final int n)
    {
        return shiftAndFill((hGridX + m * hRegionY + n * hRegionX));
    }

    private int computeY(final int m, final int n)
    {
        return shiftAndFill((hGridY + m * hRegionX - n * hRegionY));
    }

    private int shiftAndFill(int value)
    {
        // shift value by 8 and let the leftmost 8 bits be 0
        value >>= 8;

        if (value < 0)
        {
            // fill the leftmost 8 bits with 1
            final int bitPosition = (int) (Math.log(Integer.highestOneBit(value)) / Math.log(2));

            for (int i = 1; i < 31 - bitPosition; i++)
            {
                // bit flip
                value |= 1 << (31 - i);
            }
        }

        return value;
    }

    public void init(final SegmentHeader header, final SubInputStream sis)
            throws InvalidHeaderValueException, IOException
    {
        this.segmentHeader = header;
        this.subInputStream = sis;
        this.regionInfo = new RegionSegmentInformation(subInputStream);
        parseHeader();
    }

    public CombinationOperator getCombinationOperator()
    {
        return hCombinationOperator;
    }

    public RegionSegmentInformation getRegionInfo()
    {
        return regionInfo;
    }

    protected byte getHTemplate()
    {
        return hTemplate;
    }

    protected boolean isHSkipEnabled()
    {
        return hSkipEnabled;
    }

    protected boolean isMMREncoded()
    {
        return isMMREncoded;
    }

    protected int getHGridWidth()
    {
        return hGridWidth;
    }

    protected int getHGridHeight()
    {
        return hGridHeight;
    }

    protected int getHGridX()
    {
        return hGridX;
    }

    protected int getHGridY()
    {
        return hGridY;
    }

    protected int getHRegionX()
    {
        return hRegionX;
    }

    protected int getHRegionY()
    {
        return hRegionY;
    }

    protected byte getHDefaultPixel()
    {
        return hDefaultPixel;
    }
}
