/*

   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.batik.ext.awt;

import java.awt.Color;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;

/**
 * Provides the actual implementation for the LinearGradientPaint
 * This is where the pixel processing is done.
 *
 * @author Nicholas Talian, Vincent Hardy, Jim Graham, Jerry Evans
 * @author <a href="mailto:vincent.hardy@eng.sun.com">Vincent Hardy</a>
 * @version $Id$
 * @see java.awt.PaintContext
 * @see java.awt.Paint
 * @see java.awt.GradientPaint
 */
final class LinearGradientPaintContext extends MultipleGradientPaintContext {

    /**
     * The following invariants are used to process the gradient value from
     * a device space coordinate, (X, Y):
     * g(X, Y) = dgdX*X + dgdY*Y + gc
     */
    private float dgdX, dgdY, gc, pixSz;

    private static final int DEFAULT_IMPL = 1;
    private static final int ANTI_ALIAS_IMPL  = 3;

    private int fillMethod;

    /**
     * Constructor for LinearGradientPaintContext.
     *
     *  @param cm {@link ColorModel} that receives
     *  the <code>Paint</code> data. This is used only as a hint.
     *
     *  @param deviceBounds the device space bounding box of the
     *  graphics primitive being rendered
     *
     *  @param userBounds the user space bounding box of the
     *  graphics primitive being rendered
     *
     *  @param t the {@link AffineTransform} from user
     *  space into device space (gradientTransform should be
     *  concatenated with this)
     *
     *  @param hints the hints that the context object uses to choose
     *  between rendering alternatives
     *
     *  @param dStart gradient start point, in user space
     *
     *  @param dEnd gradient end point, in user space
     *
     *  @param fractions the fractions specifying the gradient distribution
     *
     *  @param colors the gradient colors
     *
     *  @param cycleMethod either NO_CYCLE, REFLECT, or REPEAT
     *
     *  @param colorSpace which colorspace to use for interpolation,
     *  either SRGB or LINEAR_RGB
     *
     */
    public LinearGradientPaintContext(ColorModel cm,
                                      Rectangle deviceBounds,
                                      Rectangle2D userBounds,
                                      AffineTransform t,
                                      RenderingHints hints,
                                      Point2D dStart,
                                      Point2D dEnd,
                                      float[] fractions,
                                      Color[] colors,
                                      MultipleGradientPaint.CycleMethodEnum
                                      cycleMethod,
                                      MultipleGradientPaint.ColorSpaceEnum
                                      colorSpace)
        throws NoninvertibleTransformException
    {
        super(cm, deviceBounds, userBounds, t, hints, fractions,
              colors, cycleMethod, colorSpace);

        // Use single precision floating points
        Point2D.Float start = new Point2D.Float((float)dStart.getX(),
                                                (float)dStart.getY());
        Point2D.Float end = new Point2D.Float((float)dEnd.getX(),
                                              (float)dEnd.getY());

        // A given point in the raster should take on the same color as its
        // projection onto the gradient vector.
        // Thus, we want the projection of the current position vector
        // onto the gradient vector, then normalized with respect to the
        // length of the gradient vector, giving a value which can be mapped into
        // the range 0-1.
        // projection = currentVector dot gradientVector / length(gradientVector)
        // normalized = projection / length(gradientVector)

        float dx = end.x - start.x; // change in x from start to end
        float dy = end.y - start.y; // change in y from start to end
        float dSq = dx*dx + dy*dy; // total distance squared

        //avoid repeated calculations by doing these divides once.
        float constX = dx/dSq;
        float constY = dy/dSq;

        //incremental change along gradient for +x
        dgdX = a00*constX + a10*constY;
        //incremental change along gradient for +y
        dgdY = a01*constX + a11*constY;

        float dgdXAbs = Math.abs(dgdX);
        float dgdYAbs = Math.abs(dgdY);
        if (dgdXAbs > dgdYAbs)  pixSz = dgdXAbs;
        else                    pixSz = dgdYAbs;

        //constant, incorporates the translation components from the matrix
        gc = (a02-start.x)*constX + (a12-start.y)*constY;

        Object colorRend = hints.get(RenderingHints.KEY_COLOR_RENDERING);
        Object rend      = hints.get(RenderingHints.KEY_RENDERING);

        fillMethod = DEFAULT_IMPL;

        if ((cycleMethod == MultipleGradientPaint.REPEAT) ||
            hasDiscontinuity) {
            if (rend      == RenderingHints.VALUE_RENDER_QUALITY)
                fillMethod = ANTI_ALIAS_IMPL;
            // ColorRend overrides rend.
            if (colorRend == RenderingHints.VALUE_COLOR_RENDER_SPEED)
                fillMethod = DEFAULT_IMPL;
            else if (colorRend == RenderingHints.VALUE_COLOR_RENDER_QUALITY)
                fillMethod = ANTI_ALIAS_IMPL;
        }
    }

    protected void fillHardNoCycle(int[] pixels, int off, int adjust,
                              int x, int y, int w, int h) {

        //constant which can be pulled out of the inner loop
        final float initConst = (dgdX*x) + gc;

        for(int i=0; i<h; i++) { //for every row
            //initialize current value to be start.
            float g = initConst + dgdY*(y+i);
            final int rowLimit = off+w;  // end of row iteration

            if (dgdX == 0) {
                // System.out.println("In fillHard: " + g);
                final int val;
                if (g <= 0)
                    val = gradientUnderflow;
                else if (g >= 1)
                    val = gradientOverflow;
                else {
                    // Could be a binary search...
                    int gradIdx = 0;
                    while (gradIdx < gradientsLength-1) {
                        if (g < fractions[gradIdx+1])
                            break;
                        gradIdx++;
                    }
                    float delta = (g-fractions[gradIdx]);
                    float idx  = ((delta*GRADIENT_SIZE_INDEX)
                                  /normalizedIntervals[gradIdx])+0.5f;
                    val = gradients[gradIdx][(int)idx];
                }

                while (off < rowLimit) {
                    pixels[off++] = val;
                }
            } else {
                // System.out.println("In fillHard2: " + g);
                int gradSteps;
                int preGradSteps;
                final int preVal, postVal;

                float gradStepsF;
                float preGradStepsF;
                if (dgdX >= 0) {
                    gradStepsF    =          ((1-g)/dgdX);
                    preGradStepsF = (float)Math.ceil((0-g)/dgdX);
                    preVal  = gradientUnderflow;
                    postVal = gradientOverflow;
                } else { // dgdX < 0
                    gradStepsF    =          ((0-g)/dgdX);
                    preGradStepsF = (float)Math.ceil((1-g)/dgdX);
                    preVal  = gradientOverflow;
                    postVal = gradientUnderflow;
                }

                if (gradStepsF > w)    gradSteps = w;
                else                   gradSteps = (int)gradStepsF;
                if (preGradStepsF > w) preGradSteps = w;
                else                   preGradSteps = (int)preGradStepsF;

                final int gradLimit    = off + gradSteps;
                if (preGradSteps > 0) {
                    final int preGradLimit = off + preGradSteps;

                    while (off < preGradLimit) {
                        pixels[off++] = preVal;
                    }
                    g += dgdX*preGradSteps;
                }

                if (dgdX > 0) {
                    // Could be a binary search...
                    int gradIdx = 0;
                    while (gradIdx < gradientsLength-1) {
                        if (g < fractions[gradIdx+1])
                            break;
                        gradIdx++;
                    }

                    while (off < gradLimit) {
                        float delta = (g-fractions[gradIdx]);
                        final int [] grad = gradients[gradIdx];

                        double stepsD = Math.ceil
                            ((fractions[gradIdx+1]-g)/dgdX);
                        int steps;
                        if (stepsD > w) steps = w;
                        else            steps = (int)stepsD;
                        int subGradLimit = off + steps;
                        if (subGradLimit > gradLimit)
                            subGradLimit = gradLimit;

                        int idx  = (int)(((delta*GRADIENT_SIZE_INDEX)
                                          /normalizedIntervals[gradIdx])
                                         *(1<<16)) + (1<<15);
                        int step = (int)(((dgdX*GRADIENT_SIZE_INDEX)
                                          /normalizedIntervals[gradIdx])
                                         *(1<<16));
                        while (off < subGradLimit) {
                            pixels[off++] = grad[idx>>16];
                            idx += step;
                        }
                        g+=dgdX*stepsD;
                        gradIdx++;
                    }
                } else {
                    // Could be a binary search...
                    int gradIdx = gradientsLength-1;
                    while (gradIdx > 0) {
                        if (g > fractions[gradIdx])
                            break;
                        gradIdx--;
                    }

                    while (off < gradLimit) {
                        float delta = (g-fractions[gradIdx]);
                        final int [] grad = gradients[gradIdx];

                        double stepsD     = Math.ceil(delta/-dgdX);
                        int    steps;
                        if (stepsD > w) steps = w;
                        else            steps = (int)stepsD;
                        int subGradLimit = off + steps;
                        if (subGradLimit > gradLimit)
                            subGradLimit = gradLimit;

                        int idx  = (int)(((delta*GRADIENT_SIZE_INDEX)
                                          /normalizedIntervals[gradIdx])
                                         *(1<<16)) + (1<<15);
                        int step = (int)(((dgdX*GRADIENT_SIZE_INDEX)
                                          /normalizedIntervals[gradIdx])
                                         *(1<<16));
                        while (off < subGradLimit) {
                            pixels[off++] = grad[idx>>16];
                            idx += step;
                        }
                        g+=dgdX*stepsD;
                        gradIdx--;
                    }
                }

                while (off < rowLimit) {
                    pixels[off++] = postVal;
                }
            }
            off += adjust; //change in off from row to row
        }
    }

    protected void fillSimpleNoCycle(int[] pixels, int off, int adjust,
                                int x, int y, int w, int h) {
        //constant which can be pulled out of the inner loop
        final float initConst = (dgdX*x) + gc;
        final float      step = dgdX*fastGradientArraySize;
        final int      fpStep = (int)(step*(1<<16));  // fix point step

        final int [] grad = gradient;

        for(int i=0; i<h; i++){ //for every row
            //initialize current value to be start.
            float g = initConst + dgdY*(y+i);
            g *= fastGradientArraySize;
            g += 0.5; // rounding factor...

            final int rowLimit = off+w;  // end of row iteration

            float check = dgdX*fastGradientArraySize*w;
            if (check < 0) check = -check;
            if (check < .3) {
                // System.out.println("In fillSimpleNC: " + g);
                final int val;
                if (g<=0)
                    val = gradientUnderflow;
                else if (g>=fastGradientArraySize)
                    val = gradientOverflow;
                else
                    val = grad[(int)g];
                while (off < rowLimit) {
                    pixels[off++] = val;
                }
            } else {
                // System.out.println("In fillSimpleNC2: " + g);
                int gradSteps;
                int preGradSteps;
                final int preVal, postVal;
                if (dgdX > 0) {
                    gradSteps = (int)((fastGradientArraySize-g)/step);
                    preGradSteps = (int)Math.ceil(0-g/step);
                    preVal  = gradientUnderflow;
                    postVal = gradientOverflow;

                } else { // dgdX < 0
                    gradSteps    = (int)((0-g)/step);
                    preGradSteps =
                        (int)Math.ceil((fastGradientArraySize-g)/step);
                    preVal  = gradientOverflow;
                    postVal = gradientUnderflow;
                }

                if (gradSteps > w)
                    gradSteps = w;
                final int gradLimit    = off + gradSteps;

                if (preGradSteps > 0) {
                    if (preGradSteps > w)
                        preGradSteps = w;
                    final int preGradLimit = off + preGradSteps;

                    while (off < preGradLimit) {
                        pixels[off++] = preVal;
                    }
                    g += step*preGradSteps;
                }

                int fpG = (int)(g*(1<<16));
                while (off < gradLimit) {
                    pixels[off++] = grad[fpG>>16];
                    fpG += fpStep;
                }

                while (off < rowLimit) {
                    pixels[off++] = postVal;
                }
            }
            off += adjust; //change in off from row to row
        }
    }

    protected void fillSimpleRepeat(int[] pixels, int off, int adjust,
                               int x, int y, int w, int h) {

        final float initConst = (dgdX*x) + gc;

        // Limit step to fractional part of
        // fastGradientArraySize (the non fractional part has
        // no affect anyways, and would mess up lots of stuff
        // below).
        float step = (dgdX - (int)dgdX)*fastGradientArraySize;

                // Make it a Positive step (a small negative step is
                // the same as a positive step slightly less than
                // fastGradientArraySize.
        if (step < 0)
            step += fastGradientArraySize;

        final int [] grad = gradient;

        for(int i=0; i<h; i++) { //for every row
            //initialize current value to be start.
            float g = initConst + dgdY*(y+i);

            // now Limited between -1 and 1.
            g = g-(int)g;
            // put in the positive side.
            if (g < 0)
                g += 1;

            // scale for gradient array...
            g *= fastGradientArraySize;
            g += 0.5; // rounding factor
            final int rowLimit = off+w;  // end of row iteration
            while (off < rowLimit) {
                int idx = (int)g;
                if (idx >= fastGradientArraySize) {
                    g   -= fastGradientArraySize;
                    idx -= fastGradientArraySize;
                }
                pixels[off++] = grad[idx];
                g += step;
            }

            off += adjust; //change in off from row to row
        }
    }

    protected void fillSimpleReflect(int[] pixels, int off, int adjust,
                                int x, int y, int w, int h) {
        final float initConst = (dgdX*x) + gc;

        final int [] grad = gradient;

        for (int i=0; i<h; i++) { //for every row
            //initialize current value to be start.
            float g = initConst + dgdY*(y+i);

            // now limited g to -2<->2
            g = g - 2*((int)(g/2.0f));

            float step = dgdX;
            // Pull it into the positive half
            if (g < 0) {
                g = -g; //take absolute value
                step = - step;  // Change direction..
            }

            // Now do the same for dgdX. This is safe because
            // any step that is a multiple of 2.0 has no
            // affect, hence we can remove it which the first
            // part does.  The second part simply adds 2.0
            // (which has no affect due to the cylcle) to move
            // all negative step values into the positive
            // side.
            step = step - 2*((int)step/2.0f);
            if (step < 0)
                step += 2.0;
            final int reflectMax = 2*fastGradientArraySize;

            // Scale for gradient array.
            g    *= fastGradientArraySize;
            g    += 0.5;
            step *= fastGradientArraySize;
            final int rowLimit = off+w;  // end of row iteration
            while (off < rowLimit) {
                int idx = (int)g;
                if (idx >= reflectMax) {
                    g   -= reflectMax;
                    idx -= reflectMax;
                }

                if (idx <= fastGradientArraySize)
                    pixels[off++] = grad[idx];
                else
                    pixels[off++] = grad[reflectMax-idx];
                g+= step;
            }

            off += adjust; //change in off from row to row
        }
    }

    /**
     * Return a Raster containing the colors generated for the graphics
     * operation.  This is where the area is filled with colors distributed
     * linearly.
     *
     * @param x The x coordinate of the area in device space for which colors
     *          are generated.
     * @param y The y coordinate of the area in device space for which colors
     *          are generated.
     * @param w The width of the area in device space for which colors
     *          are generated.
     * @param h The height of the area in device space for which colors
     *          are generated.
     */
    protected void fillRaster(int[] pixels, int off, int adjust,
                              int x, int y, int w, int h) {

        //constant which can be pulled out of the inner loop
        final float initConst = (dgdX*x) + gc;

        if (fillMethod == ANTI_ALIAS_IMPL) {
            //initialize current value to be start.
            for(int i=0; i<h; i++){ //for every row
                float g = initConst + dgdY*(y+i);

                final int rowLimit = off+w;  // end of row iteration
                while(off < rowLimit){ //for every pixel in this row.
                    //get the color
                    pixels[off++] = indexGradientAntiAlias(g, pixSz);
                    g += dgdX; //incremental change in g
                }
                off += adjust; //change in off from row to row
            }
        }
        else if (!isSimpleLookup) {
            if (cycleMethod == MultipleGradientPaint.NO_CYCLE) {
                fillHardNoCycle(pixels, off, adjust, x, y, w, h);
            }
            else {
                //initialize current value to be start.
                for(int i=0; i<h; i++){ //for every row
                    float g = initConst + dgdY*(y+i);

                    final int rowLimit = off+w;  // end of row iteration
                    while(off < rowLimit){ //for every pixel in this row.
                        //get the color
                        pixels[off++] = indexIntoGradientsArrays(g);
                        g += dgdX; //incremental change in g
                    }
                    off += adjust; //change in off from row to row
                }
            }
        } else {
            // Simple implementations: just scale index by array size

            if (cycleMethod == MultipleGradientPaint.NO_CYCLE)
                fillSimpleNoCycle(pixels, off, adjust, x, y, w, h);
            else if (cycleMethod == MultipleGradientPaint.REPEAT)
                fillSimpleRepeat(pixels, off, adjust, x, y, w, h);
            else //cycleMethod == MultipleGradientPaint.REFLECT
                fillSimpleReflect(pixels, off, adjust, x, y, w, h);
        }
    }
}
