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

import java.awt.Rectangle;
import java.awt.image.WritableRaster;

import org.apache.pdfbox.jbig2.Bitmap;
import org.apache.pdfbox.jbig2.util.Utils;

class Resizer
{

    static final class Mapping
    {
        /** x and y scales */
        final double scale;

        /** x and y offset used by MAP, private fields */
        final double offset = .5;

        private final double a0;
        private final double b0;

        Mapping(double a0, double aw, double b0, double bw)
        {
            this.a0 = a0;
            this.b0 = b0;
            scale = bw / aw;

            if (scale <= 0.)
                throw new IllegalArgumentException("Negative scales are not allowed");
        }

        Mapping(double scaleX)
        {
            scale = scaleX;
            a0 = b0 = 0;
        }

        double mapPixelCenter(final int b)
        {
            return (b + offset - b0) / scale + a0;
        }

        double dstToSrc(final double b)
        {
            return (b - b0) / scale + a0;
        }

        double srcToDst(final double a)
        {
            return (a - a0) * scale + b0;
        }
    }

    /**
     * Order in which to apply filter
     */
    private enum Order
    {
        AUTO, XY, YX
    }

    /** Error tolerance */
    private static final double EPSILON = 1e-7;

    /** Number of bits in filter coefficients */
    private int weightBits = 14;

    private int weightOne = 1 << weightBits;

    /** Number of bits per channel */
    private int bitsPerChannel[] = new int[] { 8, 8, 8 };

    private static final int NO_SHIFT[] = new int[16];

    private int finalShift[] = new int[] { 2 * weightBits - bitsPerChannel[0],
            2 * weightBits - bitsPerChannel[1], 2 * weightBits - bitsPerChannel[2] };

    /**
     * Is x an integer?
     * 
     * @param x the double to check
     * @return <code>true</code> if x is an integer, <code>false</code> if not.
     */
    private static boolean isInteger(final double x)
    {
        return Math.abs(x - Math.floor(x + .5)) < EPSILON;
    }

    static final boolean debug = false;

    /**
     * Should filters be simplified if possible?
     */
    private final boolean coerce = true;

    /**
     * The order in which data is processed.
     * 
     * @see Order
     */
    private final Order order = Order.AUTO;

    /**
     * Should zeros be trimmed in x filter weight tables?
     */
    private final boolean trimZeros = true;

    private final Mapping mappingX;
    private final Mapping mappingY;

    /**
     * Creates an instance of {@link Resizer} with one scale factor for both x and y directions.
     * 
     * @param scale the scale factor for x and y direction
     */
    public Resizer(double scale)
    {
        this(scale, scale);
    }

    /**
     * Creates an instance of {@link Resizer} with a scale factor for each direction.
     * 
     * @param scaleX the scale factor for x direction
     * @param scaleY the scale factor for y direction
     */
    public Resizer(double scaleX, double scaleY)
    {
        mappingX = new Mapping(scaleX);
        mappingY = new Mapping(scaleY);
    }

    private Weighttab[] createXWeights(Rectangle srcBounds, final Rectangle dstBounds,
            final ParameterizedFilter filter)
    {
        final int srcX0 = srcBounds.x;
        final int srcX1 = srcBounds.x + srcBounds.width;

        final int dstX0 = dstBounds.x;
        final int dstX1 = dstBounds.x + dstBounds.width;

        final Weighttab tabs[] = new Weighttab[dstBounds.width];
        for (int dstX = dstX0; dstX < dstX1; dstX++)
        {
            final double center = mappingX.mapPixelCenter(dstX);
            tabs[dstX - dstX0] = new Weighttab(filter, weightOne, center, srcX0, srcX1 - 1,
                    trimZeros);
        }

        return tabs;
    }

    /**
     * Checks if our discrete sampling of an arbitrary continuous filter, parameterized by the filter spacing
     * ({@link ParameterizedFilter#scale}), its radius ({@link ParameterizedFilter#support}), and the scale and offset
     * of the coordinate mapping, causes the filter to reduce to point sampling.
     * <p>
     * It reduces if support is less than 1 pixel or if integer scale and translation, and filter is cardinal.
     * 
     * @param filter the parameterized filter instance to be simplified
     * @param scale the scale of the coordinate mapping
     * @param offset the offset of the coordinate mapping
     */
    private ParameterizedFilter simplifyFilter(final ParameterizedFilter filter, final double scale,
            final double offset)
    {
        if (coerce && (filter.support <= .5 || filter.filter.cardinal
                && isInteger(1. / filter.scale) && isInteger(1. / (scale * filter.scale))
                && isInteger((offset / scale - .5) / filter.scale)))
            return new ParameterizedFilter(new Filter.Point(), 1., .5, 1);

        return filter;
    }

    /**
     * Filtered zoom, x direction filtering before y direction filtering
     * <p>
     * Note: when calling {@link Resizer#createXWeights(Rectangle, Rectangle, ParameterizedFilter)}, we can trim leading
     * and trailing zeros from the x weight buffers as an optimization, but not for y weight buffers since the split
     * formula is anticipating a constant amount of buffering of source scanlines; trimming zeros in y weight could
     * cause feedback.
     */
    private void resizeXfirst(final Object src, final Rectangle srcBounds, final Object dst,
            final Rectangle dstBounds, final ParameterizedFilter xFilter,
            final ParameterizedFilter yFilter)
    {
        // source scanline buffer
        final Scanline buffer = createScanline(src, dst, srcBounds.width);

        // accumulator buffer
        final Scanline accumulator = createScanline(src, dst, dstBounds.width);

        // a sampled filter for source pixels for each dest x position
        final Weighttab xWeights[] = createXWeights(srcBounds, dstBounds, xFilter);

        // Circular buffer of active lines
        final int yBufferSize = yFilter.width + 2;
        final Scanline lineBuffer[] = new Scanline[yBufferSize];
        for (int y = 0; y < yBufferSize; y++)
        {
            lineBuffer[y] = createScanline(src, dst, dstBounds.width);
            lineBuffer[y].y = -1; /* mark scanline as unread */
        }

        // range of source and destination scanlines in regions
        final int srcY0 = srcBounds.y;
        final int srcY1 = srcBounds.y + srcBounds.height;
        final int dstY0 = dstBounds.y;
        final int dstY1 = dstBounds.y + dstBounds.height;

        int yFetched = -1; // used to assert no backtracking

        // loop over dest scanlines
        for (int dstY = dstY0; dstY < dstY1; dstY++)
        {
            // a sampled filter for source pixels for each dest x position
            final Weighttab yWeight = new Weighttab(yFilter, weightOne,
                    mappingY.mapPixelCenter(dstY), srcY0, srcY1 - 1, true);

            accumulator.clear();

            // loop over source scanlines that contribute to this dest scanline
            for (int srcY = yWeight.i0; srcY <= yWeight.i1; srcY++)
            {
                final Scanline srcBuffer = lineBuffer[srcY % yBufferSize];

                if (debug)
                    System.out.println("  abuf.y / ayf " + srcBuffer.y + " / " + srcY);

                if (srcBuffer.y != srcY)
                {
                    // scanline needs to be fetched from src raster
                    srcBuffer.y = srcY;

                    if (srcY0 + srcY <= yFetched)
                        throw new AssertionError(
                                "Backtracking from line " + yFetched + " to " + (srcY0 + srcY));

                    buffer.fetch(srcBounds.x, srcY0 + srcY);

                    yFetched = srcY0 + srcY;

                    // filter it into the appropriate line of linebuf (xfilt)
                    buffer.filter(NO_SHIFT, bitsPerChannel, xWeights, srcBuffer);
                }

                // add weighted tbuf into accum (these do yfilt)
                srcBuffer.accumulate(yWeight.weights[srcY - yWeight.i0], accumulator);
            }

            accumulator.shift(finalShift);
            accumulator.store(dstBounds.x, dstY);
            if (debug)
                System.out.printf("\n");
        }
    }

    /**
     * Filtered zoom, y direction filtering before x direction filtering
     */
    private void resizeYfirst(final Object src, final Rectangle srcBounds, final Object dst,
            final Rectangle dstBounds, final ParameterizedFilter xFilter,
            final ParameterizedFilter yFilter)
    {
        // destination scanline buffer
        final Scanline buffer = createScanline(src, dst, dstBounds.width);

        // accumulator buffer
        final Scanline accumulator = createScanline(src, dst, srcBounds.width);

        // a sampled filter for source pixels for each destination x position
        final Weighttab xWeights[] = createXWeights(srcBounds, dstBounds, xFilter);

        // Circular buffer of active lines
        final int yBufferSize = yFilter.width + 2;
        final Scanline lineBuffer[] = new Scanline[yBufferSize];
        for (int y = 0; y < yBufferSize; y++)
        {
            lineBuffer[y] = createScanline(src, dst, srcBounds.width);

            // mark scanline as unread
            lineBuffer[y].y = -1;
        }

        // range of source and destination scanlines in regions
        final int srcY0 = srcBounds.y;
        final int srcY1 = srcBounds.y + srcBounds.height;
        final int dstY0 = dstBounds.y;
        final int dstY1 = dstBounds.y + dstBounds.height;

        // used to assert no backtracking
        int yFetched = -1;

        // loop over destination scanlines
        for (int dstY = dstY0; dstY < dstY1; dstY++)
        {
            // prepare a weighttab for destination y position by a single sampled filter for current y
            // position
            final Weighttab yWeight = new Weighttab(yFilter, weightOne,
                    mappingY.mapPixelCenter(dstY), srcY0, srcY1 - 1, true);

            accumulator.clear();

            // loop over source scanlines that contribute to this destination scanline
            for (int srcY = yWeight.i0; srcY <= yWeight.i1; srcY++)
            {
                final Scanline srcBuffer = lineBuffer[srcY % yBufferSize];
                if (srcBuffer.y != srcY)
                {
                    // scanline needs to be fetched from source raster
                    srcBuffer.y = srcY;

                    if (srcY0 + srcY <= yFetched)
                        throw new AssertionError(
                                "Backtracking from line " + yFetched + " to " + (srcY0 + srcY));

                    srcBuffer.fetch(srcBounds.x, srcY0 + srcY);

                    yFetched = srcY0 + srcY;
                }

                if (debug)
                    System.out.println(
                            dstY + "[] += " + srcY + "[] * " + yWeight.weights[srcY - yWeight.i0]);

                // add weighted source buffer into accumulator (these do y filter)
                srcBuffer.accumulate(yWeight.weights[srcY - yWeight.i0], accumulator);
            }

            // and filter it into the appropriate line of line buffer (x filter)
            accumulator.filter(bitsPerChannel, finalShift, xWeights, buffer);

            // store destination scanline into destination raster
            buffer.store(dstBounds.x, dstY);
            if (debug)
                System.out.printf("\n");
        }
    }

    /**
     * @param src Source object
     * @param srcBounds Bounds of the source object
     * @param dst Destination object
     * @param dstBounds Bounds of the destination object
     * @param xFilter The filter used for x direction filtering
     * @param yFilter The filter used for y direction filtering
     */
    public void resize(final Object src, final Rectangle srcBounds, final Object dst,
            Rectangle dstBounds, Filter xFilter, Filter yFilter)
    {
        /*
         * find scale of filter in a space (source space) when minifying, source scale=1/scale, but when magnifying,
         * source scale=1
         */
        ParameterizedFilter xFilterParameterized = new ParameterizedFilter(xFilter, mappingX.scale);
        ParameterizedFilter yFilterParameterized = new ParameterizedFilter(yFilter, mappingY.scale);

        /* find valid destination window (transformed source + support margin) */
        final Rectangle dstRegion = new Rectangle();
        final int x1 = Utils
                .ceil(mappingX.srcToDst(srcBounds.x - xFilterParameterized.support) + EPSILON);
        final int y1 = Utils
                .ceil(mappingY.srcToDst(srcBounds.y - yFilterParameterized.support) + EPSILON);
        final int x2 = Utils.floor(
                mappingX.srcToDst(srcBounds.x + srcBounds.width + xFilterParameterized.support)
                        - EPSILON);
        final int y2 = Utils.floor(
                mappingY.srcToDst(srcBounds.y + srcBounds.height + yFilterParameterized.support)
                        - EPSILON);
        dstRegion.setFrameFromDiagonal(x1, y1, x2, y2);

        if (dstBounds.x < dstRegion.x || dstBounds.getMaxX() > dstRegion.getMaxX()
                || dstBounds.y < dstRegion.y || dstBounds.getMaxY() > dstRegion.getMaxY())
        {
            /* requested destination window lies outside the valid destination, so clip destination */
            dstBounds = dstBounds.intersection(dstRegion);
        }

        if (srcBounds.isEmpty() || dstBounds.width <= 0 || dstBounds.height <= 0)
        {
            return;
        }

        /* check for high-level simplifications of filter */
        xFilterParameterized = simplifyFilter(xFilterParameterized, mappingX.scale,
                mappingX.offset);
        yFilterParameterized = simplifyFilter(yFilterParameterized, mappingY.scale,
                mappingY.offset);

        /*
         * decide which filtering order (x->y or y->x) is faster for this mapping by counting convolution multiplies
         */
        final boolean orderXY = order != Order.AUTO ? order == Order.XY
                : dstBounds.width * (srcBounds.height * xFilterParameterized.width
                        + dstBounds.height * yFilterParameterized.width) < dstBounds.height
                                * (dstBounds.width * xFilterParameterized.width
                                        + srcBounds.width * yFilterParameterized.width);

        // choose most efficient filtering order
        if (orderXY)
        {
            resizeXfirst(src, srcBounds, dst, dstBounds, xFilterParameterized,
                    yFilterParameterized);
        }
        else
        {
            resizeYfirst(src, srcBounds, dst, dstBounds, xFilterParameterized,
                    yFilterParameterized);
        }
    }

    private static Scanline createScanline(final Object src, Object dst, final int length)
    {
        if (src == null)
            throw new IllegalArgumentException("src must not be null");

        if (!(src instanceof Bitmap))
            throw new IllegalArgumentException("src must be from type " + Bitmap.class.getName());

        if (dst == null)
            throw new IllegalArgumentException("dst must not be null");

        if (!(dst instanceof WritableRaster))
            throw new IllegalArgumentException(
                    "dst must be from type " + WritableRaster.class.getName());

        return new BitmapScanline((Bitmap) src, (WritableRaster) dst, length);
    }

}
