/*
 * Copyright (c) 2007, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/**********************************************************************
 **********************************************************************
 **********************************************************************
 *** COPYRIGHT (c) Eastman Kodak Company, 1997                      ***
 *** As  an unpublished  work pursuant to Title 17 of the United    ***
 *** States Code.  All rights reserved.                             ***
 **********************************************************************
 **********************************************************************
 **********************************************************************/

package sun.java2d.cmm.lcms;

import java.awt.color.ICC_Profile;
import java.awt.color.ProfileDataException;
import java.awt.color.CMMException;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.awt.image.ColorModel;
import java.awt.image.DirectColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.SampleModel;
import java.awt.image.DataBuffer;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.ComponentSampleModel;
import sun.java2d.cmm.*;
import sun.java2d.cmm.lcms.*;
import static sun.java2d.cmm.lcms.LCMSImageLayout.ImageLayoutException;


public class LCMSTransform implements ColorTransform {
    long ID;
    private int inFormatter = 0;
    private boolean isInIntPacked = false;
    private int outFormatter = 0;
    private boolean isOutIntPacked = false;

    ICC_Profile[] profiles;
    LCMSProfile[] lcmsProfiles;
    int renderType;
    int transformType;

    private int numInComponents = -1;
    private int numOutComponents = -1;

    private Object disposerReferent = new Object();

    /* the class initializer */
    static {
        if (ProfileDeferralMgr.deferring) {
            ProfileDeferralMgr.activateProfiles();
        }
    }

    public LCMSTransform(ICC_Profile profile, int renderType,
                         int transformType)
    {
        /* Actually, it is not a complete transform but just part of it */
        profiles = new ICC_Profile[1];
        profiles[0] = profile;
        lcmsProfiles = new LCMSProfile[1];
        lcmsProfiles[0] = LCMS.getProfileID(profile);
        this.renderType = (renderType == ColorTransform.Any)?
                              ICC_Profile.icPerceptual : renderType;
        this.transformType = transformType;

        /* Note that ICC_Profile.getNumComponents() is quite expensive
         * (it may results in a reading of the profile header).
         * So, here we cache the number of components of input and
         * output profiles for further usage.
         */
        numInComponents = profiles[0].getNumComponents();
        numOutComponents = profiles[profiles.length - 1].getNumComponents();
    }

    public LCMSTransform (ColorTransform[] transforms) {
        int size = 0;
        for (int i=0; i < transforms.length; i++) {
            size+=((LCMSTransform)transforms[i]).profiles.length;
        }
        profiles = new ICC_Profile[size];
        lcmsProfiles = new LCMSProfile[size];
        int j = 0;
        for (int i=0; i < transforms.length; i++) {
            LCMSTransform curTrans = (LCMSTransform)transforms[i];
            System.arraycopy(curTrans.profiles, 0, profiles, j,
                             curTrans.profiles.length);
            System.arraycopy(curTrans.lcmsProfiles, 0, lcmsProfiles, j,
                             curTrans.lcmsProfiles.length);
            j += curTrans.profiles.length;
        }
        renderType = ((LCMSTransform)transforms[0]).renderType;

        /* Note that ICC_Profile.getNumComponents() is quite expensive
         * (it may results in a reading of the profile header).
         * So, here we cache the number of components of input and
         * output profiles for further usage.
         */
        numInComponents = profiles[0].getNumComponents();
        numOutComponents = profiles[profiles.length - 1].getNumComponents();
    }

    public int getNumInComponents() {
        return numInComponents;
    }

    public int getNumOutComponents() {
        return numOutComponents;
    }

    private synchronized void doTransform(LCMSImageLayout in,
                                          LCMSImageLayout out) {
        // update native transfrom if needed
        if (ID == 0L ||
            inFormatter != in.pixelType || isInIntPacked != in.isIntPacked ||
            outFormatter != out.pixelType || isOutIntPacked != out.isIntPacked)
        {

            if (ID != 0L) {
                // Disposer will destroy forgotten transform
                disposerReferent = new Object();
            }
            inFormatter = in.pixelType;
            isInIntPacked = in.isIntPacked;

            outFormatter = out.pixelType;
            isOutIntPacked = out.isIntPacked;

            ID = LCMS.createTransform(lcmsProfiles, renderType,
                                            inFormatter, isInIntPacked,
                                            outFormatter, isOutIntPacked,
                                            disposerReferent);
        }

        LCMS.colorConvert(this, in, out);
    }

    public void colorConvert(BufferedImage src, BufferedImage dst) {
        LCMSImageLayout srcIL, dstIL;
        try {
            if (!dst.getColorModel().hasAlpha()) {
                dstIL = LCMSImageLayout.createImageLayout(dst);

                if (dstIL != null) {
                    srcIL = LCMSImageLayout.createImageLayout(src);
                    if (srcIL != null) {
                        doTransform(srcIL, dstIL);
                        return;
                    }
                }
            }
        }  catch (ImageLayoutException e) {
            throw new CMMException("Unable to convert images");
        }

        Raster srcRas = src.getRaster();
        WritableRaster dstRas = dst.getRaster();
        ColorModel srcCM = src.getColorModel();
        ColorModel dstCM = dst.getColorModel();
        int w = src.getWidth();
        int h = src.getHeight();
        int srcNumComp = srcCM.getNumColorComponents();
        int dstNumComp = dstCM.getNumColorComponents();
        int precision = 8;
        float maxNum = 255.0f;
        for (int i = 0; i < srcNumComp; i++) {
            if (srcCM.getComponentSize(i) > 8) {
                 precision = 16;
                 maxNum = 65535.0f;
             }
        }
        for (int i = 0; i < dstNumComp; i++) {
            if (dstCM.getComponentSize(i) > 8) {
                 precision = 16;
                 maxNum = 65535.0f;
             }
        }
        float[] srcMinVal = new float[srcNumComp];
        float[] srcInvDiffMinMax = new float[srcNumComp];
        ColorSpace cs = srcCM.getColorSpace();
        for (int i = 0; i < srcNumComp; i++) {
            srcMinVal[i] = cs.getMinValue(i);
            srcInvDiffMinMax[i] = maxNum / (cs.getMaxValue(i) - srcMinVal[i]);
        }
        cs = dstCM.getColorSpace();
        float[] dstMinVal = new float[dstNumComp];
        float[] dstDiffMinMax = new float[dstNumComp];
        for (int i = 0; i < dstNumComp; i++) {
            dstMinVal[i] = cs.getMinValue(i);
            dstDiffMinMax[i] = (cs.getMaxValue(i) - dstMinVal[i]) / maxNum;
        }
        boolean dstHasAlpha = dstCM.hasAlpha();
        boolean needSrcAlpha = srcCM.hasAlpha() && dstHasAlpha;
        float[] dstColor;
        if (dstHasAlpha) {
            dstColor = new float[dstNumComp + 1];
        } else {
            dstColor = new float[dstNumComp];
        }
        if (precision == 8) {
            byte[] srcLine = new byte[w * srcNumComp];
            byte[] dstLine = new byte[w * dstNumComp];
            Object pixel;
            float[] color;
            float[] alpha = null;
            if (needSrcAlpha) {
                alpha = new float[w];
            }
            int idx;
            // TODO check for src npixels = dst npixels
            try {
                srcIL = new LCMSImageLayout(
                        srcLine, srcLine.length/getNumInComponents(),
                        LCMSImageLayout.CHANNELS_SH(getNumInComponents()) |
                        LCMSImageLayout.BYTES_SH(1), getNumInComponents());
                dstIL = new LCMSImageLayout(
                        dstLine, dstLine.length/getNumOutComponents(),
                        LCMSImageLayout.CHANNELS_SH(getNumOutComponents()) |
                        LCMSImageLayout.BYTES_SH(1), getNumOutComponents());
            } catch (ImageLayoutException e) {
                throw new CMMException("Unable to convert images");
            }
            // process each scanline
            for (int y = 0; y < h; y++) {
                // convert src scanline
                pixel = null;
                color = null;
                idx = 0;
                for (int x = 0; x < w; x++) {
                    pixel = srcRas.getDataElements(x, y, pixel);
                    color = srcCM.getNormalizedComponents(pixel, color, 0);
                    for (int i = 0; i < srcNumComp; i++) {
                        srcLine[idx++] = (byte)
                            ((color[i] - srcMinVal[i]) * srcInvDiffMinMax[i] +
                             0.5f);
                    }
                    if (needSrcAlpha) {
                        alpha[x] = color[srcNumComp];
                    }
                }
                // color convert srcLine to dstLine
                doTransform(srcIL, dstIL);

                // convert dst scanline
                pixel = null;
                idx = 0;
                for (int x = 0; x < w; x++) {
                    for (int i = 0; i < dstNumComp; i++) {
                        dstColor[i] = ((float) (dstLine[idx++] & 0xff)) *
                                      dstDiffMinMax[i] + dstMinVal[i];
                    }
                    if (needSrcAlpha) {
                        dstColor[dstNumComp] = alpha[x];
                    } else if (dstHasAlpha) {
                        dstColor[dstNumComp] = 1.0f;
                    }
                    pixel = dstCM.getDataElements(dstColor, 0, pixel);
                    dstRas.setDataElements(x, y, pixel);
                }
            }
        } else {
            short[] srcLine = new short[w * srcNumComp];
            short[] dstLine = new short[w * dstNumComp];
            Object pixel;
            float[] color;
            float[] alpha = null;
            if (needSrcAlpha) {
                alpha = new float[w];
            }
            int idx;
            try {
                srcIL = new LCMSImageLayout(
                    srcLine, srcLine.length/getNumInComponents(),
                    LCMSImageLayout.CHANNELS_SH(getNumInComponents()) |
                    LCMSImageLayout.BYTES_SH(2), getNumInComponents()*2);

                dstIL = new LCMSImageLayout(
                    dstLine, dstLine.length/getNumOutComponents(),
                    LCMSImageLayout.CHANNELS_SH(getNumOutComponents()) |
                    LCMSImageLayout.BYTES_SH(2), getNumOutComponents()*2);
            } catch (ImageLayoutException e) {
                throw new CMMException("Unable to convert images");
            }
            // process each scanline
            for (int y = 0; y < h; y++) {
                // convert src scanline
                pixel = null;
                color = null;
                idx = 0;
                for (int x = 0; x < w; x++) {
                    pixel = srcRas.getDataElements(x, y, pixel);
                    color = srcCM.getNormalizedComponents(pixel, color, 0);
                    for (int i = 0; i < srcNumComp; i++) {
                        srcLine[idx++] = (short)
                            ((color[i] - srcMinVal[i]) * srcInvDiffMinMax[i] +
                             0.5f);
                    }
                    if (needSrcAlpha) {
                        alpha[x] = color[srcNumComp];
                    }
                }
                // color convert srcLine to dstLine
                doTransform(srcIL, dstIL);

                // convert dst scanline
                pixel = null;
                idx = 0;
                for (int x = 0; x < w; x++) {
                    for (int i = 0; i < dstNumComp; i++) {
                        dstColor[i] = ((float) (dstLine[idx++] & 0xffff)) *
                                      dstDiffMinMax[i] + dstMinVal[i];
                    }
                    if (needSrcAlpha) {
                        dstColor[dstNumComp] = alpha[x];
                    } else if (dstHasAlpha) {
                        dstColor[dstNumComp] = 1.0f;
                    }
                    pixel = dstCM.getDataElements(dstColor, 0, pixel);
                    dstRas.setDataElements(x, y, pixel);
                }
            }
        }
    }

    public void colorConvert(Raster src, WritableRaster dst,
                             float[] srcMinVal, float[]srcMaxVal,
                             float[] dstMinVal, float[]dstMaxVal) {
        LCMSImageLayout srcIL, dstIL;

        // Can't pass src and dst directly to CMM, so process per scanline
        SampleModel srcSM = src.getSampleModel();
        SampleModel dstSM = dst.getSampleModel();
        int srcTransferType = src.getTransferType();
        int dstTransferType = dst.getTransferType();
        boolean srcIsFloat, dstIsFloat;
        if ((srcTransferType == DataBuffer.TYPE_FLOAT) ||
            (srcTransferType == DataBuffer.TYPE_DOUBLE)) {
            srcIsFloat = true;
        } else {
            srcIsFloat = false;
        }
        if ((dstTransferType == DataBuffer.TYPE_FLOAT) ||
            (dstTransferType == DataBuffer.TYPE_DOUBLE)) {
            dstIsFloat = true;
        } else {
            dstIsFloat = false;
        }
        int w = src.getWidth();
        int h = src.getHeight();
        int srcNumBands = src.getNumBands();
        int dstNumBands = dst.getNumBands();
        float[] srcScaleFactor = new float[srcNumBands];
        float[] dstScaleFactor = new float[dstNumBands];
        float[] srcUseMinVal = new float[srcNumBands];
        float[] dstUseMinVal = new float[dstNumBands];
        for (int i = 0; i < srcNumBands; i++) {
            if (srcIsFloat) {
                srcScaleFactor[i] =  65535.0f / (srcMaxVal[i] - srcMinVal[i]);
                srcUseMinVal[i] = srcMinVal[i];
            } else {
                if (srcTransferType == DataBuffer.TYPE_SHORT) {
                    srcScaleFactor[i] = 65535.0f / 32767.0f;
                } else {
                    srcScaleFactor[i] = 65535.0f /
                        ((float) ((1 << srcSM.getSampleSize(i)) - 1));
                }
                srcUseMinVal[i] = 0.0f;
            }
        }
        for (int i = 0; i < dstNumBands; i++) {
            if (dstIsFloat) {
                dstScaleFactor[i] = (dstMaxVal[i] - dstMinVal[i]) / 65535.0f;
                dstUseMinVal[i] = dstMinVal[i];
            } else {
                if (dstTransferType == DataBuffer.TYPE_SHORT) {
                    dstScaleFactor[i] = 32767.0f / 65535.0f;
                } else {
                    dstScaleFactor[i] =
                        ((float) ((1 << dstSM.getSampleSize(i)) - 1)) /
                        65535.0f;
                }
                dstUseMinVal[i] = 0.0f;
            }
        }
        int ys = src.getMinY();
        int yd = dst.getMinY();
        int xs, xd;
        float sample;
        short[] srcLine = new short[w * srcNumBands];
        short[] dstLine = new short[w * dstNumBands];
        int idx;
        try {
            srcIL = new LCMSImageLayout(
                    srcLine, srcLine.length/getNumInComponents(),
                    LCMSImageLayout.CHANNELS_SH(getNumInComponents()) |
                    LCMSImageLayout.BYTES_SH(2), getNumInComponents()*2);

            dstIL = new LCMSImageLayout(
                    dstLine, dstLine.length/getNumOutComponents(),
                    LCMSImageLayout.CHANNELS_SH(getNumOutComponents()) |
                    LCMSImageLayout.BYTES_SH(2), getNumOutComponents()*2);
        } catch (ImageLayoutException e) {
            throw new CMMException("Unable to convert rasters");
        }
        // process each scanline
        for (int y = 0; y < h; y++, ys++, yd++) {
            // get src scanline
            xs = src.getMinX();
            idx = 0;
            for (int x = 0; x < w; x++, xs++) {
                for (int i = 0; i < srcNumBands; i++) {
                    sample = src.getSampleFloat(xs, ys, i);
                    srcLine[idx++] = (short)
                        ((sample - srcUseMinVal[i]) * srcScaleFactor[i] + 0.5f);
                }
            }

            // color convert srcLine to dstLine
            doTransform(srcIL, dstIL);

            // store dst scanline
            xd = dst.getMinX();
            idx = 0;
            for (int x = 0; x < w; x++, xd++) {
                for (int i = 0; i < dstNumBands; i++) {
                    sample = ((dstLine[idx++] & 0xffff) * dstScaleFactor[i]) +
                             dstUseMinVal[i];
                    dst.setSample(xd, yd, i, sample);
                }
            }
        }
    }

    public void colorConvert(Raster src, WritableRaster dst) {

        LCMSImageLayout srcIL, dstIL;
        dstIL = LCMSImageLayout.createImageLayout(dst);
        if (dstIL != null) {
            srcIL = LCMSImageLayout.createImageLayout(src);
            if (srcIL != null) {
                doTransform(srcIL, dstIL);
                return;
            }
        }
        // Can't pass src and dst directly to CMM, so process per scanline
        SampleModel srcSM = src.getSampleModel();
        SampleModel dstSM = dst.getSampleModel();
        int srcTransferType = src.getTransferType();
        int dstTransferType = dst.getTransferType();
        int w = src.getWidth();
        int h = src.getHeight();
        int srcNumBands = src.getNumBands();
        int dstNumBands = dst.getNumBands();
        int precision = 8;
        float maxNum = 255.0f;
        for (int i = 0; i < srcNumBands; i++) {
            if (srcSM.getSampleSize(i) > 8) {
                 precision = 16;
                 maxNum = 65535.0f;
             }
        }
        for (int i = 0; i < dstNumBands; i++) {
            if (dstSM.getSampleSize(i) > 8) {
                 precision = 16;
                 maxNum = 65535.0f;
             }
        }
        float[] srcScaleFactor = new float[srcNumBands];
        float[] dstScaleFactor = new float[dstNumBands];
        for (int i = 0; i < srcNumBands; i++) {
            if (srcTransferType == DataBuffer.TYPE_SHORT) {
                srcScaleFactor[i] = maxNum / 32767.0f;
            } else {
                srcScaleFactor[i] = maxNum /
                    ((float) ((1 << srcSM.getSampleSize(i)) - 1));
            }
        }
        for (int i = 0; i < dstNumBands; i++) {
            if (dstTransferType == DataBuffer.TYPE_SHORT) {
                dstScaleFactor[i] = 32767.0f / maxNum;
            } else {
                dstScaleFactor[i] =
                    ((float) ((1 << dstSM.getSampleSize(i)) - 1)) / maxNum;
            }
        }
        int ys = src.getMinY();
        int yd = dst.getMinY();
        int xs, xd;
        int sample;
        if (precision == 8) {
            byte[] srcLine = new byte[w * srcNumBands];
            byte[] dstLine = new byte[w * dstNumBands];
            int idx;
            // TODO check for src npixels = dst npixels
            try {
                srcIL = new LCMSImageLayout(
                        srcLine, srcLine.length/getNumInComponents(),
                        LCMSImageLayout.CHANNELS_SH(getNumInComponents()) |
                        LCMSImageLayout.BYTES_SH(1), getNumInComponents());
                dstIL = new LCMSImageLayout(
                        dstLine, dstLine.length/getNumOutComponents(),
                        LCMSImageLayout.CHANNELS_SH(getNumOutComponents()) |
                        LCMSImageLayout.BYTES_SH(1), getNumOutComponents());
            } catch (ImageLayoutException e) {
                throw new CMMException("Unable to convert rasters");
            }
            // process each scanline
            for (int y = 0; y < h; y++, ys++, yd++) {
                // get src scanline
                xs = src.getMinX();
                idx = 0;
                for (int x = 0; x < w; x++, xs++) {
                    for (int i = 0; i < srcNumBands; i++) {
                        sample = src.getSample(xs, ys, i);
                        srcLine[idx++] = (byte)
                            ((sample * srcScaleFactor[i]) + 0.5f);
                    }
                }

                // color convert srcLine to dstLine
                doTransform(srcIL, dstIL);

                // store dst scanline
                xd = dst.getMinX();
                idx = 0;
                for (int x = 0; x < w; x++, xd++) {
                    for (int i = 0; i < dstNumBands; i++) {
                        sample = (int) (((dstLine[idx++] & 0xff) *
                                         dstScaleFactor[i]) + 0.5f);
                        dst.setSample(xd, yd, i, sample);
                    }
                }
            }
        } else {
            short[] srcLine = new short[w * srcNumBands];
            short[] dstLine = new short[w * dstNumBands];
            int idx;

            try {
                srcIL = new LCMSImageLayout(
                        srcLine, srcLine.length/getNumInComponents(),
                        LCMSImageLayout.CHANNELS_SH(getNumInComponents()) |
                        LCMSImageLayout.BYTES_SH(2), getNumInComponents()*2);

                dstIL = new LCMSImageLayout(
                        dstLine, dstLine.length/getNumOutComponents(),
                        LCMSImageLayout.CHANNELS_SH(getNumOutComponents()) |
                        LCMSImageLayout.BYTES_SH(2), getNumOutComponents()*2);
            } catch (ImageLayoutException e) {
                throw new CMMException("Unable to convert rasters");
            }
            // process each scanline
            for (int y = 0; y < h; y++, ys++, yd++) {
                // get src scanline
                xs = src.getMinX();
                idx = 0;
                for (int x = 0; x < w; x++, xs++) {
                    for (int i = 0; i < srcNumBands; i++) {
                        sample = src.getSample(xs, ys, i);
                        srcLine[idx++] = (short)
                            ((sample * srcScaleFactor[i]) + 0.5f);
                    }
                }

                // color convert srcLine to dstLine
                doTransform(srcIL, dstIL);

                // store dst scanline
                xd = dst.getMinX();
                idx = 0;
                for (int x = 0; x < w; x++, xd++) {
                    for (int i = 0; i < dstNumBands; i++) {
                        sample = (int) (((dstLine[idx++] & 0xffff) *
                                         dstScaleFactor[i]) + 0.5f);
                        dst.setSample(xd, yd, i, sample);
                    }
                }
            }
        }
    }

    /* convert an array of colors in short format */
    /* each color is a contiguous set of array elements */
    /* the number of colors is (size of the array) / (number of input/output
       components */
    public short[] colorConvert(short[] src, short[] dst) {

        if (dst == null) {
            dst = new short [(src.length/getNumInComponents())*getNumOutComponents()];
        }

        try {
            LCMSImageLayout srcIL = new LCMSImageLayout(
                    src, src.length/getNumInComponents(),
                    LCMSImageLayout.CHANNELS_SH(getNumInComponents()) |
                    LCMSImageLayout.BYTES_SH(2), getNumInComponents()*2);

            LCMSImageLayout dstIL = new LCMSImageLayout(
                    dst, dst.length/getNumOutComponents(),
                    LCMSImageLayout.CHANNELS_SH(getNumOutComponents()) |
                    LCMSImageLayout.BYTES_SH(2), getNumOutComponents()*2);

            doTransform(srcIL, dstIL);

            return dst;
        } catch (ImageLayoutException e) {
            throw new CMMException("Unable to convert data");
        }
    }

    public byte[] colorConvert(byte[] src, byte[] dst) {
        if (dst == null) {
            dst = new byte [(src.length/getNumInComponents())*getNumOutComponents()];
        }

        try {
            LCMSImageLayout srcIL = new LCMSImageLayout(
                    src, src.length/getNumInComponents(),
                    LCMSImageLayout.CHANNELS_SH(getNumInComponents()) |
                    LCMSImageLayout.BYTES_SH(1), getNumInComponents());

            LCMSImageLayout dstIL = new LCMSImageLayout(
                    dst, dst.length/getNumOutComponents(),
                    LCMSImageLayout.CHANNELS_SH(getNumOutComponents()) |
                    LCMSImageLayout.BYTES_SH(1), getNumOutComponents());

            doTransform(srcIL, dstIL);

            return dst;
        } catch (ImageLayoutException e) {
            throw new CMMException("Unable to convert data");
        }
    }
}
