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

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.apache.pdfbox.jbig2.err.IntegerMaxValueException;
import org.apache.pdfbox.jbig2.err.InvalidHeaderValueException;
import org.apache.pdfbox.jbig2.err.JBIG2Exception;
import org.apache.pdfbox.jbig2.image.Bitmaps;
import org.apache.pdfbox.jbig2.segments.EndOfStripe;
import org.apache.pdfbox.jbig2.segments.PageInformation;
import org.apache.pdfbox.jbig2.segments.RegionSegmentInformation;
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 a JBIG2 page.
 */
class JBIG2Page
{

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

    /**
     * This list contains all segments of this page, sorted by segment number in ascending order.
     */
    private final Map<Integer, SegmentHeader> segments = new TreeMap<Integer, SegmentHeader>();

    /** NOTE: page number != segmentList index */
    private final int pageNumber;

    /** The page bitmap that represents the page buffer */
    private Bitmap pageBitmap;

    private int finalHeight;
    private int finalWidth;
    private int resolutionX;
    private int resolutionY;

    private final JBIG2Document document;

    protected JBIG2Page(JBIG2Document document, int pageNumber)
    {
        this.document = document;
        this.pageNumber = pageNumber;
    }

    /**
     * This method searches for a segment specified by its number.
     * 
     * @param number - Segment number of the segment to search.
     * 
     * @return The retrieved {@link SegmentHeader} or {@code null} if not available.
     */
    public SegmentHeader getSegment(int number)
    {
        SegmentHeader s = segments.get(number);

        if (null != s)
        {
            return s;
        }

        if (null != document)
        {
            return document.getGlobalSegment(number);
        }

        log.info("Segment not found, returning null.");
        return null;
    }

    /**
     * Returns the associated page information segment.
     * 
     * @return The associated {@link PageInformation} segment or {@code null} if not available.
     */
    protected SegmentHeader getPageInformationSegment()
    {
        for (SegmentHeader s : segments.values())
        {
            if (s.getSegmentType() == 48)
            {
                return s;
            }
        }

        log.info("Page information segment not found.");
        return null;
    }

    /**
     * This method returns the decoded bitmap if present. Otherwise the page bitmap will be composed before returning
     * the result.
     * 
     * @return pageBitmap - The result of decoding a page
     * @throws JBIG2Exception
     * @throws IOException
     */
    protected Bitmap getBitmap() throws JBIG2Exception, IOException
    {
        long timestamp;

        if (JBIG2ImageReader.PERFORMANCE_TEST)
        {
            timestamp = System.currentTimeMillis();
        }

        if (null == pageBitmap)
        {
            composePageBitmap();
        }

        if (JBIG2ImageReader.PERFORMANCE_TEST)
        {
            log.info("PAGE DECODING: " + (System.currentTimeMillis() - timestamp) + " ms");
        }

        return pageBitmap;
    }

    /**
     * This method composes the segments' bitmaps to a page and stores the page as a {@link Bitmap}
     * 
     * @throws IOException
     * @throws JBIG2Exception
     */
    private void composePageBitmap() throws IOException, JBIG2Exception
    {
        if (pageNumber > 0)
        {
            // Page 79, 1) Decoding the page information segment
            PageInformation pageInformation = (PageInformation) getPageInformationSegment()
                    .getSegmentData();
            createPage(pageInformation);
            clearSegmentData();
        }
    }

    private void createPage(PageInformation pageInformation)
            throws IOException, IntegerMaxValueException, InvalidHeaderValueException
    {
        if (!pageInformation.isStriped() || pageInformation.getHeight() != -1)
        {
            // Page 79, 4)
            createNormalPage(pageInformation);
        }
        else
        {
            createStripedPage(pageInformation);
        }
    }

    private void createNormalPage(PageInformation pageInformation)
            throws IOException, IntegerMaxValueException, InvalidHeaderValueException
    {

        pageBitmap = new Bitmap(pageInformation.getWidth(), pageInformation.getHeight());

        // Page 79, 3)
        // If default pixel value is not 0, byte will be filled with 0xff
        if (pageInformation.getDefaultPixelValue() != 0)
        {
            Arrays.fill(pageBitmap.getByteArray(), (byte) 0xff);
        }

        for (SegmentHeader s : segments.values())
        {
            // Page 79, 5)
            switch (s.getSegmentType())
            {
            case 6: // Immediate text region
            case 7: // Immediate lossless text region
            case 22: // Immediate halftone region
            case 23: // Immediate lossless halftone region
            case 38: // Immediate generic region
            case 39: // Immediate lossless generic region
            case 42: // Immediate generic refinement region
            case 43: // Immediate lossless generic refinement region
                final Region r = (Region) s.getSegmentData();

                final Bitmap regionBitmap = r.getRegionBitmap();

                if (fitsPage(pageInformation, regionBitmap))
                {
                    pageBitmap = regionBitmap;
                }
                else
                {
                    final RegionSegmentInformation regionInfo = r.getRegionInfo();
                    final CombinationOperator op = getCombinationOperator(pageInformation,
                            regionInfo.getCombinationOperator());
                    Bitmaps.blit(regionBitmap, pageBitmap, regionInfo.getXLocation(),
                            regionInfo.getYLocation(), op);
                }

                break;
            }
        }
    }

    /**
     * Check if we have only one region that forms the complete page. If the dimension equals the page's dimension set
     * the region's bitmap as the page's bitmap. Otherwise we have to blit the smaller region's bitmap into the page's
     * bitmap (see Issue 6).
     * 
     * @param pageInformation
     * @param regionBitmap
     * @return
     */
    private boolean fitsPage(PageInformation pageInformation, final Bitmap regionBitmap)
    {
        return countRegions() == 1 && pageInformation.getDefaultPixelValue() == 0
                && pageInformation.getWidth() == regionBitmap.getWidth()
                && pageInformation.getHeight() == regionBitmap.getHeight();
    }

    private void createStripedPage(PageInformation pageInformation)
            throws IOException, IntegerMaxValueException, InvalidHeaderValueException
    {
        final ArrayList<SegmentData> pageStripes = collectPageStripes();

        pageBitmap = new Bitmap(pageInformation.getWidth(), finalHeight);

        int startLine = 0;
        for (SegmentData sd : pageStripes)
        {
            if (sd instanceof EndOfStripe)
            {
                startLine = ((EndOfStripe) sd).getLineNumber() + 1;
            }
            else
            {
                final Region r = (Region) sd;
                final RegionSegmentInformation regionInfo = r.getRegionInfo();
                final CombinationOperator op = getCombinationOperator(pageInformation,
                        regionInfo.getCombinationOperator());
                Bitmaps.blit(r.getRegionBitmap(), pageBitmap, regionInfo.getXLocation(), startLine,
                        op);
            }
        }
    }

    private ArrayList<SegmentData> collectPageStripes()
    {
        final ArrayList<SegmentData> pageStripes = new ArrayList<SegmentData>();
        for (SegmentHeader s : segments.values())
        {
            // Page 79, 5)
            switch (s.getSegmentType())
            {
            case 6: // Immediate text region
            case 7: // Immediate lossless text region
            case 22: // Immediate halftone region
            case 23: // Immediate lossless halftone region
            case 38: // Immediate generic region
            case 39: // Immediate lossless generic region
            case 42: // Immediate generic refinement region
            case 43: // Immediate lossless generic refinement region
                Region r = (Region) s.getSegmentData();
                pageStripes.add(r);
                break;

            case 50: // End of stripe
                EndOfStripe eos = (EndOfStripe) s.getSegmentData();
                pageStripes.add(eos);
                finalHeight = eos.getLineNumber() + 1;
                break;
            }
        }

        return pageStripes;
    }

    /**
     * This method counts the regions segments. If there is only one region, the bitmap of this segment is equal to the
     * page bitmap and blitting is not necessary.
     * 
     * @return Amount of regions.
     */
    private int countRegions()
    {
        int regionCount = 0;

        for (SegmentHeader s : segments.values())
        {
            switch (s.getSegmentType())
            {
            case 6: // Immediate text region
            case 7: // Immediate lossless text region
            case 22: // Immediate halftone region
            case 23: // Immediate lossless halftone region
            case 38: // Immediate generic region
            case 39: // Immediate lossless generic region
            case 42: // Immediate generic refinement region
            case 43: // Immediate lossless generic refinement region
                regionCount++;
            }
        }

        return regionCount;
    }

    /**
     * This method checks and sets, which combination operator shall be used.
     * 
     * @param pi - <code>PageInformation</code> object
     * @param newOperator - The combination operator, specified by actual segment
     * @return the new combination operator
     */
    private CombinationOperator getCombinationOperator(PageInformation pi,
            CombinationOperator newOperator)
    {
        if (pi.isCombinationOperatorOverrideAllowed())
        {
            return newOperator;
        }
        else
        {
            return pi.getCombinationOperator();
        }
    }

    /**
     * Adds a {@link SegmentHeader} into the page's segments map.
     * 
     * @param segment - The segment to be added.
     */
    protected void add(SegmentHeader segment)
    {

        segments.put(segment.getSegmentNr(), segment);
    }

    /**
     * Resets the memory-critical segments to force on-demand-decoding and to avoid holding the segments' bitmap too
     * long.
     */
    private void clearSegmentData()
    {
        Set<Integer> keySet = segments.keySet();

        for (Integer key : keySet)
        {
            segments.get(key).cleanSegmentData();
        }
    }

    /**
     * Reset memory-critical parts of page.
     */
    protected void clearPageData()
    {
        pageBitmap = null;
    }

    /**
     * Returns the final height of the page.
     * 
     * @return The final height of the page.
     * @throws IOException
     * @throws JBIG2Exception
     */
    protected int getHeight() throws IOException, JBIG2Exception
    {
        if (finalHeight == 0)
        {
            PageInformation pi = (PageInformation) getPageInformationSegment().getSegmentData();
            if (pi.getHeight() == 0xffffffff)
            {
                getBitmap();
            }
            else
            {
                finalHeight = pi.getHeight();
            }
        }
        return finalHeight;
    }

    protected int getWidth()
    {
        if (finalWidth == 0)
        {
            PageInformation pi = (PageInformation) getPageInformationSegment().getSegmentData();
            finalWidth = pi.getWidth();
        }
        return finalWidth;
    }

    protected int getResolutionX()
    {
        if (resolutionX == 0)
        {
            PageInformation pi = (PageInformation) getPageInformationSegment().getSegmentData();
            resolutionX = pi.getResolutionX();
        }
        return resolutionX;
    }

    protected int getResolutionY()
    {
        if (resolutionY == 0)
        {
            PageInformation pi = (PageInformation) getPageInformationSegment().getSegmentData();
            resolutionY = pi.getResolutionY();
        }
        return resolutionY;
    }

    @Override
    public String toString()
    {
        return getClass().getSimpleName() + " (Page number: " + pageNumber + ")";
    }
}
