/*
 * 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.pdmodel.graphics.color;

import java.awt.Point;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSNull;
import org.apache.pdfbox.pdmodel.common.function.PDFunction;

/**
 * A Separation color space used to specify either additional colorants or for isolating the
 * control of individual colour components of a device colour space for a subtractive device.
 * When such a space is the current colour space, the current colour shall be a single-component
 * value, called a tint, that controls the given colorant or colour components only.
 *
 * @author Ben Litchfield
 * @author John Hewson
 */
public class PDSeparation extends PDSpecialColorSpace
{
    private final PDColor initialColor = new PDColor(new float[] { 1 }, this);

    // array indexes
    private static final int COLORANT_NAMES = 1;
    private static final int ALTERNATE_CS = 2;
    private static final int TINT_TRANSFORM = 3;

    // fields
    private PDColorSpace alternateColorSpace = null;
    private PDFunction tintTransform = null;

    /**
     * Map used to speed up {@link #toRGB(float[])}. Note that this class contains three maps (this
     * and the two in {@link #toRGBImage(java.awt.image.WritableRaster) } and {@link #toRGBImage2(java.awt.image.WritableRaster)
     * }. The maps use different key intervals. This map here is needed for shading, which produce
     * more than 256 different float values, which we cast to int so that the map can work.
     */
    private Map<Integer, float[]> toRGBMap = null;

    /**
     * Creates a new Separation color space.
     */
    public PDSeparation()
    {
        array = new COSArray();
        array.add(COSName.SEPARATION);
        array.add(COSName.getPDFName(""));
        // add some placeholder
        array.add(COSNull.NULL);
        array.add(COSNull.NULL);
    }

    /**
     * Creates a new Separation color space from a PDF color space array.
     * @param separation an array containing all separation information.
     * @throws IOException if the color space or the function could not be created.
     */
    public PDSeparation(COSArray separation) throws IOException
    {
        array = separation;
        alternateColorSpace = PDColorSpace.create(array.getObject(ALTERNATE_CS));
        tintTransform = PDFunction.create(array.getObject(TINT_TRANSFORM));
        int numberOfOutputParameters = tintTransform.getNumberOfOutputParameters();
        if (numberOfOutputParameters > 0 &&
                numberOfOutputParameters < alternateColorSpace.getNumberOfComponents())
        {
            throw new IOException("The tint transform function has less output parameters (" +
                    tintTransform.getNumberOfOutputParameters() + ") than the alternate colorspace " +
                    alternateColorSpace + " (" + alternateColorSpace.getNumberOfComponents() + ")");
        }
    }

    @Override
    public String getName()
    {
        return COSName.SEPARATION.getName();
    }

    @Override
    public int getNumberOfComponents()
    {
        return 1;
    }

    @Override
    public float[] getDefaultDecode(int bitsPerComponent)
    {
        return new float[] { 0, 1 };
    }

    @Override
    public PDColor getInitialColor()
    {
        return initialColor;
    }

    @Override
    public float[] toRGB(float[] value) throws IOException
    {
        if (toRGBMap == null)
        {
            toRGBMap = new HashMap<>();
        }
        int key = (int) (value[0] * 255);
        float[] retval = toRGBMap.get(key);
        if (retval != null)
        {
            return retval;
        }
        float[] altColor = tintTransform.eval(value);
        retval = alternateColorSpace.toRGB(altColor);
        toRGBMap.put(key, retval);
        return retval;
    }

    //
    // WARNING: this method is performance sensitive, modify with care!
    //
    @Override
    public BufferedImage toRGBImage(WritableRaster raster) throws IOException
    {
        if (alternateColorSpace instanceof PDLab)
        {
            // PDFBOX-3622 - regular converter fails for Lab colorspaces
            return toRGBImage2(raster);
        }

        int numAltComponents = alternateColorSpace.getNumberOfComponents();

        // use the tint transform to convert the sample into
        // the alternate color space (this is usually 1:many)
        WritableRaster altRaster = Raster.createBandedRaster(DataBuffer.TYPE_BYTE,
                raster.getWidth(), raster.getHeight(), numAltComponents, new Point(0, 0));

        int width = raster.getWidth();
        int height = raster.getHeight();
        float[] samples = new float[1];

        Map<Integer, int[]> calculatedValues = new HashMap<>();
        Integer hash;
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                raster.getPixel(x, y, samples);
                hash = Float.floatToIntBits(samples[0]);
                int[] alt = calculatedValues.get(hash);
                if (alt == null)
                {
                    alt = new int[numAltComponents];
                    tintTransform(samples, alt);
                    calculatedValues.put(hash, alt);
                }                
                altRaster.setPixel(x, y, alt);
            }
        }

        // convert the alternate color space to RGB
        return alternateColorSpace.toRGBImage(altRaster);
    }

    // converter that works without using super implementation of toRGBImage()
    private BufferedImage toRGBImage2(WritableRaster raster) throws IOException
    {
        int width = raster.getWidth();
        int height = raster.getHeight();
        BufferedImage rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        WritableRaster rgbRaster = rgbImage.getRaster();
        float[] samples = new float[1];

        Map<Integer, int[]> calculatedValues = new HashMap<>();
        Integer hash;
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                raster.getPixel(x, y, samples);
                hash = Float.floatToIntBits(samples[0]);
                int[] rgb = calculatedValues.get(hash);
                if (rgb == null)
                {
                    samples[0] /= 255;
                    float[] altColor = tintTransform.eval(samples);
                    float[] fltab = alternateColorSpace.toRGB(altColor);
                    rgb = new int[3];
                    rgb[0] = (int) (fltab[0] * 255);
                    rgb[1] = (int) (fltab[1] * 255);
                    rgb[2] = (int) (fltab[2] * 255);
                    calculatedValues.put(hash, rgb);
                }
                rgbRaster.setPixel(x, y, rgb);
            }
        }
        return rgbImage;
    }

    protected void tintTransform(float[] samples, int[] alt) throws IOException
    {
        samples[0] /= 255; // 0..1
        float[] result = tintTransform.eval(samples);
        for (int s = 0; s < alt.length; s++)
        {
            // scale to 0..255
            alt[s] = (int) (result[s] * 255);
        }
    }

    @Override
    public BufferedImage toRawImage(WritableRaster raster)
    {
        return toRawImage(raster, ColorSpace.getInstance(ColorSpace.CS_GRAY));
    }

    /**
     * Returns the colorant name.
     * @return the name of the colorant
     */
    public PDColorSpace getAlternateColorSpace()
    {
       return alternateColorSpace;
    }

    /**
     * Returns the colorant name.
     * @return the name of the colorant
     */
    public String getColorantName()
    {
        COSName name = (COSName)array.getObject(COLORANT_NAMES);
        return name.getName();
    }

    /**
     * Sets the colorant name.
     * @param name the name of the colorant
     */
    public void setColorantName(String name)
    {
        array.set(1, COSName.getPDFName(name));
    }

    /**
     * Sets the alternate color space.
     * @param colorSpace The alternate color space.
     */
    public void setAlternateColorSpace(PDColorSpace colorSpace)
    {
        alternateColorSpace = colorSpace;
        COSBase space = null;
        if (colorSpace != null)
        {
            space = colorSpace.getCOSObject();
        }
        array.set(ALTERNATE_CS, space);
    }

    /**
     * Sets the tint transform function.
     * @param tint the tint transform function
     */
    public void setTintTransform(PDFunction tint)
    {
        tintTransform = tint;
        array.set(TINT_TRANSFORM, tint);
    }

    @Override
    public String toString()
    {
        return getName() + "{" +
                "\"" + getColorantName() + "\"" + " " +
                alternateColorSpace.getName() + " " +
                tintTransform + "}";
    }
}
