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

package org.apache.xmlgraphics.image.rendered;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.color.ColorSpace;
import java.awt.image.BandCombineOp;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;

import org.apache.xmlgraphics.image.GraphicsUtil;

// CSOFF: ConstantName
// CSOFF: NeedBraces
// CSOFF: WhitespaceAfter
// CSOFF: WhitespaceAround

/**
 * This function will tranform an image from any colorspace into a
 * luminance image.  The alpha channel if any will be copied to the
 * new image.
 *
 * @version $Id$
 *
 * Originally authored by Thomas DeWeese.
 */
public class Any2LsRGBRed extends AbstractRed {

    boolean srcIssRGB;

    /**
     * Construct a luminace image from src.
     *
     * @param src The image to convert to a luminance image
     */
    public Any2LsRGBRed(CachableRed src) {
        super(src, src.getBounds(),
              fixColorModel(src),
              fixSampleModel(src),
              src.getTileGridXOffset(),
              src.getTileGridYOffset(),
              null);

        ColorModel srcCM = src.getColorModel();
        if (srcCM == null) {
            return;
        }
        ColorSpace srcCS = srcCM.getColorSpace();
        if (srcCS == ColorSpace.getInstance(ColorSpace.CS_sRGB)) {
            srcIssRGB = true;
        }
    }

    /**
     * Gamma for linear to sRGB convertion
     */
    private static final double GAMMA = 2.4;
    private static final double LFACT = 1.0 / 12.92;


    public static final double sRGBToLsRGB(double value) {
        if (value <= 0.003928) {
            return value * LFACT;
        }
        return Math.pow((value + 0.055) / 1.055, GAMMA);
    }

    /**
     * Lookup tables for RGB lookups. The linearToSRGBLut is used
     * when noise values are considered to be on a linearScale. The
     * linearToLinear table is used when the values are considered to
     * be on the sRGB scale to begin with.
     */
    private static final int[] sRGBToLsRGBLut = new int[256];
    static {
        final double scale = 1.0 / 255;

        // System.out.print("S2L: ");
        for (int i = 0; i < 256; i++) {
            double value = sRGBToLsRGB(i * scale);
            sRGBToLsRGBLut[i] = (int)Math.round(value * 255.0);
            // System.out.print(sRGBToLsRGBLut[i] + ",");
        }
        // System.out.println("");
    }

    public WritableRaster copyData(WritableRaster wr) {
        // Get my source.
        CachableRed src   = (CachableRed)getSources().get(0);
        ColorModel  srcCM = src.getColorModel();
        SampleModel srcSM = src.getSampleModel();

        // Fast case, SRGB source, INT Pack writable raster...
        if (srcIssRGB
            && Any2sRGBRed.is_INT_PACK_COMP(wr.getSampleModel())) {
            src.copyData(wr);
            if (srcCM.hasAlpha()) {
                GraphicsUtil.coerceData(wr, srcCM, false);
            }
            Any2sRGBRed.applyLut_INT(wr, sRGBToLsRGBLut);
            return wr;
        }

        if (srcCM == null) {
            // We don't really know much about this source, let's
            // guess based on the number of bands...

            float [][] matrix = null;
            switch (srcSM.getNumBands()) {
            case 1:
                matrix = new float[1][3];
                matrix[0][0] = 1; // Red
                matrix[0][1] = 1; // Grn
                matrix[0][2] = 1; // Blu
                break;
            case 2:
                matrix = new float[2][4];
                matrix[0][0] = 1; // Red
                matrix[0][1] = 1; // Grn
                matrix[0][2] = 1; // Blu
                matrix[1][3] = 1; // Alpha
                break;
            case 3:
                matrix = new float[3][3];
                matrix[0][0] = 1; // Red
                matrix[1][1] = 1; // Grn
                matrix[2][2] = 1; // Blu
                break;
            default:
                matrix = new float[srcSM.getNumBands()][4];
                matrix[0][0] = 1; // Red
                matrix[1][1] = 1; // Grn
                matrix[2][2] = 1; // Blu
                matrix[3][3] = 1; // Alpha
                break;
            }

            Raster srcRas = src.getData(wr.getBounds());
            BandCombineOp op = new BandCombineOp(matrix, null);
            op.filter(srcRas, wr);
        } else {
            ColorModel dstCM = getColorModel();
            BufferedImage dstBI;

            if (!dstCM.hasAlpha()) {
                // No alpha ao we don't have to work around the bug
                // in the color convert op.
                dstBI = new BufferedImage(
                    dstCM, wr.createWritableTranslatedChild(0, 0),
                     dstCM.isAlphaPremultiplied(), null);
            } else {
                // All this nonsense is to work around the fact that
                // the Color convert op doesn't properly copy the
                // Alpha from src to dst.
                SinglePixelPackedSampleModel dstSM;
                dstSM = (SinglePixelPackedSampleModel)wr.getSampleModel();
                int [] masks = dstSM.getBitMasks();
                SampleModel dstSMNoA = new SinglePixelPackedSampleModel(
                    dstSM.getDataType(), dstSM.getWidth(), dstSM.getHeight(),
                     dstSM.getScanlineStride(),
                     new int[] {masks[0], masks[1], masks[2]});
                ColorModel dstCMNoA = GraphicsUtil.Linear_sRGB;

                WritableRaster dstWr;
                dstWr = Raster.createWritableRaster(dstSMNoA,
                                                    wr.getDataBuffer(),
                                                    new Point(0, 0));
                dstWr = dstWr.createWritableChild(
                    wr.getMinX() - wr.getSampleModelTranslateX(),
                     wr.getMinY() - wr.getSampleModelTranslateY(),
                     wr.getWidth(), wr.getHeight(),
                     0, 0, null);

                dstBI = new BufferedImage(dstCMNoA, dstWr, false, null);
            }

            // Divide out alpha if we have it.  We need to do this since
            // the color convert may not be a linear operation which may
            // lead to out of range values.
            ColorModel srcBICM = srcCM;
            WritableRaster srcWr;
            if (srcCM.hasAlpha() && srcCM.isAlphaPremultiplied()) {
                Rectangle wrR = wr.getBounds();
                SampleModel sm = srcCM.createCompatibleSampleModel(
                    wrR.width, wrR.height);

                srcWr = Raster.createWritableRaster(
                    sm, new Point(wrR.x, wrR.y));
                src.copyData(srcWr);
                srcBICM = GraphicsUtil.coerceData(srcWr, srcCM, false);
            } else {
                Raster srcRas = src.getData(wr.getBounds());
                srcWr = GraphicsUtil.makeRasterWritable(srcRas);
            }

            BufferedImage srcBI;
            srcBI = new BufferedImage(srcBICM,
                                      srcWr.createWritableTranslatedChild(0, 0),
                                      false,
                                      null);

            /*
             * System.out.println("src: " + srcBI.getWidth() + "x" +
             *                    srcBI.getHeight());
             * System.out.println("dst: " + dstBI.getWidth() + "x" +
             *                    dstBI.getHeight());
             */

            ColorConvertOp op = new ColorConvertOp(null);
            op.filter(srcBI, dstBI);

            if (dstCM.hasAlpha()) {
                copyBand(srcWr, srcSM.getNumBands() - 1,
                         wr,    getSampleModel().getNumBands() - 1);
            }
        }
        return wr;
    }

        /**
         * This function 'fixes' the source's color model.  Right now
         * it just selects if it should have one or two bands based on
         * if the source had an alpha channel.
         */
    protected static ColorModel fixColorModel(CachableRed src) {
        ColorModel  cm = src.getColorModel();
        if (cm != null) {
            if (cm.hasAlpha()) {
                return GraphicsUtil.Linear_sRGB_Unpre;
            }

            return GraphicsUtil.Linear_sRGB;
        } else {
            // No ColorModel so try to make some intelligent
            // decisions based just on the number of bands...
            // 1 bands -> replicated into RGB
            // 2 bands -> Band 0 replicated into RGB & Band 1 -> alpha premult
            // 3 bands -> sRGB (not-linear?)
            // 4 bands -> sRGB premult (not-linear?)
            SampleModel sm = src.getSampleModel();

            switch (sm.getNumBands()) {
            case 1:
                return GraphicsUtil.Linear_sRGB;
            case 2:
                return GraphicsUtil.Linear_sRGB_Unpre;
            case 3:
                return GraphicsUtil.Linear_sRGB;
            default:
                return GraphicsUtil.Linear_sRGB_Unpre;
            }
        }
    }

    /**
     * This function 'fixes' the source's sample model.
     * Right now it just selects if it should have 3 or 4 bands
     * based on if the source had an alpha channel.
     */
    protected static SampleModel fixSampleModel(CachableRed src) {
        SampleModel sm = src.getSampleModel();
        ColorModel  cm = src.getColorModel();

        boolean alpha = false;

        if (cm != null) {
            alpha = cm.hasAlpha();
        } else {
            switch (sm.getNumBands()) {
            case 1: case 3:
                alpha = false;
                break;
            default:
                alpha = true;
                break;
            }
        }
        if (alpha) {
            return new SinglePixelPackedSampleModel(
                DataBuffer.TYPE_INT,
                 sm.getWidth(),
                 sm.getHeight(),
                 new int [] {0xFF0000, 0xFF00, 0xFF, 0xFF000000});
        } else {
            return new SinglePixelPackedSampleModel(
                DataBuffer.TYPE_INT,
                 sm.getWidth(),
                 sm.getHeight(),
                 new int [] {0xFF0000, 0xFF00, 0xFF});
        }
    }
}
