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

import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSName;

import java.util.HashMap;
import java.util.Map;

/**
 * Blend mode.
 *
 * @author Kühn &amp; Weyh Software GmbH
 */
public abstract class BlendMode
{
    public static final SeparableBlendMode NORMAL = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            return srcValue;
        }
    };

    public static final SeparableBlendMode COMPATIBLE = NORMAL;

    public static final SeparableBlendMode MULTIPLY = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            return srcValue * dstValue;
        }
    };

    public static final SeparableBlendMode SCREEN = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            return srcValue + dstValue - srcValue * dstValue;
        }
    };

    public static final SeparableBlendMode OVERLAY = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            return (dstValue <= 0.5) ? 2 * dstValue * srcValue : 2 * (srcValue + dstValue - srcValue
                    * dstValue) - 1;
        }
    };

    public static final SeparableBlendMode DARKEN = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            return Math.min(srcValue, dstValue);
        }
    };

    public static final SeparableBlendMode LIGHTEN = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            return Math.max(srcValue, dstValue);
        }
    };

    public static final SeparableBlendMode COLOR_DODGE = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            // See PDF 2.0 specification
            if (dstValue == 0)
            {
                return 0;
            }
            if (dstValue >= 1 - srcValue)
            {
                return 1;
            }
            return dstValue / (1 - srcValue);
        }
    };

    public static final SeparableBlendMode COLOR_BURN = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            // See PDF 2.0 specification
            if (dstValue == 1)
            {
                return 1;
            }
            if (1 - dstValue >= srcValue)
            {
                return 0;
            }
            return 1 - (1 - dstValue) / srcValue;
        }
    };

    public static final SeparableBlendMode HARD_LIGHT = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            return (srcValue <= 0.5) ? 2 * dstValue * srcValue :
                    2 * (srcValue + dstValue - srcValue * dstValue) - 1;
        }
    };

    public static final SeparableBlendMode SOFT_LIGHT = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            if (srcValue <= 0.5)
            {
                return dstValue - (1 - 2 * srcValue) * dstValue * (1 - dstValue);
            }
            else
            {
                float d = (dstValue <= 0.25) ? ((16 * dstValue - 12) * dstValue + 4) * dstValue
                        : (float) Math .sqrt(dstValue);
                return dstValue + (2 * srcValue - 1) * (d - dstValue);
            }
        }
    };

    public static final SeparableBlendMode DIFFERENCE = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            return Math.abs(dstValue - srcValue);
        }
    };

    public static final SeparableBlendMode EXCLUSION = new SeparableBlendMode()
    {
        @Override
        public float blendChannel(float srcValue, float dstValue)
        {
            return dstValue + srcValue - 2 * dstValue * srcValue;
        }
    };

    public static final NonSeparableBlendMode HUE = new NonSeparableBlendMode()
    {
        @Override
        public void blend(float[] srcValues, float[] dstValues, float[] result)
        {
            float[] temp = new float[3];
            getSaturationRGB(dstValues, srcValues, temp);
            getLuminosityRGB(dstValues, temp, result);
        }
    };

    public static final NonSeparableBlendMode SATURATION = new NonSeparableBlendMode()
    {
        @Override
        public void blend(float[] srcValues, float[] dstValues, float[] result)
        {
            getSaturationRGB(srcValues, dstValues, result);
        }
    };

    public static final NonSeparableBlendMode COLOR = new NonSeparableBlendMode()
    {
        @Override
        public void blend(float[] srcValues, float[] dstValues, float[] result)
        {
            getLuminosityRGB(dstValues, srcValues, result);
        }
    };

    public static final NonSeparableBlendMode LUMINOSITY = new NonSeparableBlendMode()
    {
        @Override
        public void blend(float[] srcValues, float[] dstValues, float[] result)
        {
            getLuminosityRGB(srcValues, dstValues, result);
        }
    };

    // these maps *must* come after the BlendMode.* constant declarations, otherwise their values would be null
    private static final Map<COSName, BlendMode> BLEND_MODES = createBlendModeMap();

    BlendMode()
    {
    }

     /**
     * Determines the blend mode from the BM entry in the COS ExtGState.
     *
     * @param cosBlendMode name or array
     * @return blending mode
     */
    public static BlendMode getInstance(COSBase cosBlendMode)
    {
        BlendMode result = null;
        if (cosBlendMode instanceof COSName)
        {
            result = BLEND_MODES.get(cosBlendMode);
        }
        else if (cosBlendMode instanceof COSArray)
        {
            COSArray cosBlendModeArray = (COSArray) cosBlendMode;
            for (int i = 0; i < cosBlendModeArray.size(); i++)
            {
                result = BLEND_MODES.get(cosBlendModeArray.getObject(i));
                if (result != null)
                {
                    break;
                }
            }
        }

        if (result != null)
        {
            return result;
        }
        return BlendMode.NORMAL;
    }

    private static int get255Value(float val)
    {
        return (int) Math.floor(val >= 1.0 ? 255 : val * 255.0);
    }

    private static void getSaturationRGB(float[] srcValues, float[] dstValues, float[] result)
    {
        int minb;
        int maxb;
        int mins;
        int maxs;
        int y;
        int scale;
        int r;
        int g;
        int b;

        int rd = get255Value(dstValues[0]);
        int gd = get255Value(dstValues[1]);
        int bd = get255Value(dstValues[2]);
        int rs = get255Value(srcValues[0]);
        int gs = get255Value(srcValues[1]);
        int bs = get255Value(srcValues[2]);

        minb = Math.min(rd, Math.min(gd, bd));
        maxb = Math.max(rd, Math.max(gd, bd));
        if (minb == maxb)
        {
            /* backdrop has zero saturation, avoid divide by 0 */
            result[0] = gd / 255.0f;
            result[1] = gd / 255.0f;
            result[2] = gd / 255.0f;
            return;
        }

        mins = Math.min(rs, Math.min(gs, bs));
        maxs = Math.max(rs, Math.max(gs, bs));

        scale = ((maxs - mins) << 16) / (maxb - minb);
        y = (rd * 77 + gd * 151 + bd * 28 + 0x80) >> 8;
        r = y + ((((rd - y) * scale) + 0x8000) >> 16);
        g = y + ((((gd - y) * scale) + 0x8000) >> 16);
        b = y + ((((bd - y) * scale) + 0x8000) >> 16);

        if (((r | g | b) & 0x100) == 0x100)
        {
            int scalemin;
            int scalemax;
            int min;
            int max;

            min = Math.min(r, Math.min(g, b));
            max = Math.max(r, Math.max(g, b));

            if (min < 0)
            {
                scalemin = (y << 16) / (y - min);
            }
            else
            {
                scalemin = 0x10000;
            }

            if (max > 255)
            {
                scalemax = ((255 - y) << 16) / (max - y);
            }
            else
            {
                scalemax = 0x10000;
            }

            scale = Math.min(scalemin, scalemax);
            r = y + (((r - y) * scale + 0x8000) >> 16);
            g = y + (((g - y) * scale + 0x8000) >> 16);
            b = y + (((b - y) * scale + 0x8000) >> 16);
        }
        result[0] = r / 255.0f;
        result[1] = g / 255.0f;
        result[2] = b / 255.0f;
    }

    private static void getLuminosityRGB(float[] srcValues, float[] dstValues, float[] result)
    {
        int delta;
        int scale;
        int r;
        int g;
        int b;
        int y;
        int rd = get255Value(dstValues[0]);
        int gd = get255Value(dstValues[1]);
        int bd = get255Value(dstValues[2]);
        int rs = get255Value(srcValues[0]);
        int gs = get255Value(srcValues[1]);
        int bs = get255Value(srcValues[2]);
        delta = ((rs - rd) * 77 + (gs - gd) * 151 + (bs - bd) * 28 + 0x80) >> 8;
        r = rd + delta;
        g = gd + delta;
        b = bd + delta;

        if (((r | g | b) & 0x100) == 0x100)
        {
            y = (rs * 77 + gs * 151 + bs * 28 + 0x80) >> 8;
            if (delta > 0)
            {
                int max;
                max = Math.max(r, Math.max(g, b));
                scale = max == y ? 0 : ((255 - y) << 16) / (max - y);
            }
            else
            {
                int min;
                min = Math.min(r, Math.min(g, b));
                scale = y == min ? 0 : (y << 16) / (y - min);
            }
            r = y + (((r - y) * scale + 0x8000) >> 16);
            g = y + (((g - y) * scale + 0x8000) >> 16);
            b = y + (((b - y) * scale + 0x8000) >> 16);
        }
        result[0] = r / 255.0f;
        result[1] = g / 255.0f;
        result[2] = b / 255.0f;
    }

    private static Map<COSName, BlendMode> createBlendModeMap()
    {
        Map<COSName, BlendMode> map = new HashMap<COSName, BlendMode>(13);
        map.put(COSName.NORMAL, BlendMode.NORMAL);
        // BlendMode.COMPATIBLE should not be used
        map.put(COSName.COMPATIBLE, BlendMode.NORMAL);
        map.put(COSName.MULTIPLY, BlendMode.MULTIPLY);
        map.put(COSName.SCREEN, BlendMode.SCREEN);
        map.put(COSName.OVERLAY, BlendMode.OVERLAY);
        map.put(COSName.DARKEN, BlendMode.DARKEN);
        map.put(COSName.LIGHTEN, BlendMode.LIGHTEN);
        map.put(COSName.COLOR_DODGE, BlendMode.COLOR_DODGE);
        map.put(COSName.COLOR_BURN, BlendMode.COLOR_BURN);
        map.put(COSName.HARD_LIGHT, BlendMode.HARD_LIGHT);
        map.put(COSName.SOFT_LIGHT, BlendMode.SOFT_LIGHT);
        map.put(COSName.DIFFERENCE, BlendMode.DIFFERENCE);
        map.put(COSName.EXCLUSION, BlendMode.EXCLUSION);
        map.put(COSName.HUE, BlendMode.HUE);
        map.put(COSName.SATURATION, BlendMode.SATURATION);
        map.put(COSName.LUMINOSITY, BlendMode.LUMINOSITY);
        map.put(COSName.COLOR, BlendMode.COLOR);
        return map;
    }
}
