/*
 * 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: PDFColorHandler.java 1903803 2022-09-01 09:36:44Z ssteiner $ */

package org.apache.fop.pdf;

import java.awt.Color;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.xmlgraphics.java2d.color.CIELabColorSpace;
import org.apache.xmlgraphics.java2d.color.ColorUtil;
import org.apache.xmlgraphics.java2d.color.ColorWithAlternatives;
import org.apache.xmlgraphics.java2d.color.DeviceCMYKColorSpace;
import org.apache.xmlgraphics.java2d.color.NamedColorSpace;
import org.apache.xmlgraphics.java2d.color.profile.ColorProfileUtil;
import org.apache.xmlgraphics.util.DoubleFormatUtil;

/**
 * This class handles the registration of color spaces and the generation of PDF code to select
 * the right colors given a {@link Color} instance.
 */
public class PDFColorHandler {

    private Log log = LogFactory.getLog(PDFColorHandler.class);

    private PDFResources resources;
    private PDFResourceContext resourceContext;

    private Map<String, PDFCIELabColorSpace> cieLabColorSpaces;

    /**
     * Create a new instance for the given {@link PDFResources}
     * @param resources the PDF resources
     */
    public PDFColorHandler(PDFResources resources, PDFResourceContext resourceContext) {
        this.resources = resources;
        this.resourceContext = resourceContext;
    }

    private PDFDocument getDocument() {
        return this.resources.getDocumentSafely();
    }

    /**
     * Generates code to select the given color and handles the registration of color spaces in
     * PDF where necessary.
     * @param codeBuffer the target buffer to receive the color selection code
     * @param color the color
     * @param fill true for fill color, false for stroke color
     */
    public void establishColor(StringBuffer codeBuffer, Color color, boolean fill, boolean alpha) {
        if (color instanceof ColorWithAlternatives) {
            ColorWithAlternatives colExt = (ColorWithAlternatives)color;
            //Alternate colors have priority
            Color[] alt = colExt.getAlternativeColors();
            for (Color col : alt) {
                boolean established = establishColorFromColor(codeBuffer, col, fill);
                if (established) {
                    return;
                }
            }
            if (log.isDebugEnabled() && alt.length > 0) {
                log.debug("None of the alternative colors are supported. Using fallback: "
                        + color);
            }
        }

        //Fallback
        boolean established = establishColorFromColor(codeBuffer, color, fill);
        if (!established) {
            establishDeviceRGB(codeBuffer, color, fill, alpha);
        }
    }

    private boolean establishColorFromColor(StringBuffer codeBuffer, Color color, boolean fill) {
        ColorSpace cs = color.getColorSpace();
        if (cs instanceof DeviceCMYKColorSpace) {
            establishDeviceCMYK(codeBuffer, color, fill);
            return true;
        } else if (!cs.isCS_sRGB()) {
            if (cs instanceof ICC_ColorSpace) {
                PDFICCBasedColorSpace pdfcs = getICCBasedColorSpace((ICC_ColorSpace)cs);
                establishColor(codeBuffer, pdfcs, color, fill);
                return true;
            } else if (cs instanceof NamedColorSpace) {
                PDFSeparationColorSpace sepcs = getSeparationColorSpace((NamedColorSpace)cs);
                establishColor(codeBuffer, sepcs, color, fill);
                return true;
            } else if (cs instanceof CIELabColorSpace) {
                CIELabColorSpace labcs = (CIELabColorSpace)cs;
                PDFCIELabColorSpace pdflab = getCIELabColorSpace(labcs);
                selectColorSpace(codeBuffer, pdflab, fill);
                float[] comps = color.getColorComponents(null);
                float[] nativeComps = labcs.toNativeComponents(comps);
                writeColor(codeBuffer, nativeComps, labcs.getNumComponents(), (fill ? "sc" : "SC"));
                return true;
            }
        }
        return false;
    }

    private PDFICCBasedColorSpace getICCBasedColorSpace(ICC_ColorSpace cs) {
        ICC_Profile profile = cs.getProfile();
        String desc = ColorProfileUtil.getICCProfileDescription(profile);
        if (log.isDebugEnabled()) {
            log.trace("ICC profile encountered: " + desc);
        }
        PDFICCBasedColorSpace pdfcs = this.resources.getICCColorSpaceByProfileName(desc);
        if (pdfcs == null) {
            //color space is not in the PDF, yet
            PDFFactory factory = getDocument().getFactory();
            PDFICCStream pdfICCStream = factory.makePDFICCStream();
            PDFDeviceColorSpace altSpace = PDFDeviceColorSpace.toPDFColorSpace(cs);
            pdfICCStream.setColorSpace(profile, altSpace);
            pdfcs = factory.makeICCBasedColorSpace(null, desc, pdfICCStream);
        }
        return pdfcs;
    }

    private PDFSeparationColorSpace getSeparationColorSpace(NamedColorSpace cs) {
        PDFName colorName = new PDFName(cs.getColorName());
        PDFSeparationColorSpace sepcs = (PDFSeparationColorSpace)this.resources.getColorSpace(
                colorName);
        if (sepcs == null) {
            //color space is not in the PDF, yet
            PDFFactory factory = getDocument().getFactory();
            sepcs = factory.makeSeparationColorSpace(null, cs);
        }
        return sepcs;
    }

    private PDFCIELabColorSpace getCIELabColorSpace(CIELabColorSpace labCS) {
        if (this.cieLabColorSpaces == null) {
            this.cieLabColorSpaces = new java.util.HashMap<String, PDFCIELabColorSpace>();
        }
        float[] wp = labCS.getWhitePoint();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 3; i++) {
            if (i > 0) {
                sb.append(',');
            }
            sb.append(wp[i]);
        }
        String key = sb.toString();
        PDFCIELabColorSpace cielab = this.cieLabColorSpaces.get(key);
        if (cielab == null) {
            //color space is not in the PDF, yet
            float[] wp1 = new float[] {wp[0] / 100f, wp[1] / 100f, wp[2] / 100f};
            cielab = new PDFCIELabColorSpace(wp1, null);
            getDocument().registerObject(cielab);
            this.resources.addColorSpace(cielab);
            this.cieLabColorSpaces.put(key, cielab);
        }
        return cielab;
    }

    private void establishColor(StringBuffer codeBuffer,
            PDFColorSpace pdfcs, Color color, boolean fill) {
        selectColorSpace(codeBuffer, pdfcs, fill);
        writeColor(codeBuffer, color, pdfcs.getNumComponents(), (fill ? "sc" : "SC"));
    }

    private void selectColorSpace(StringBuffer codeBuffer, PDFColorSpace pdfcs, boolean fill) {
        codeBuffer.append(new PDFName(pdfcs.getName()));
        if (fill)  {
            codeBuffer.append(" cs ");
        } else {
            codeBuffer.append(" CS ");
        }
    }

    private void establishDeviceRGB(StringBuffer codeBuffer, Color color, boolean fill, boolean alpha) {
        float[] comps;
        if (color.getColorSpace().isCS_sRGB()) {
            comps = color.getColorComponents(null);
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Converting color to sRGB as a fallback: " + color);
            }
            ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB);
            comps = color.getColorComponents(sRGB, null);
        }
        if (alpha) {
            writeAlpha(color, codeBuffer);
        }
        if (ColorUtil.isGray(color)) {
            comps = new float[] {comps[0]}; //assuming that all components are the same
            writeColor(codeBuffer, comps, 1, (fill ? "g" : "G"));
        } else {
            writeColor(codeBuffer, comps, 3, (fill ? "rg" : "RG"));
        }
    }

    private void writeAlpha(Color color, StringBuffer codeBuffer) {
        int alpha = color.getAlpha();
        if (alpha != 255 && alpha != 0 && resourceContext != null) {
            Map<String, Float> vals = new HashMap<>();
            vals.put(PDFGState.GSTATE_ALPHA_NONSTROKE, alpha / 255f);
            vals.put(PDFGState.GSTATE_ALPHA_STROKE, 1.0f);
            PDFGState gState = getDocument().getFactory().makeGState(vals, PDFGState.DEFAULT);
            resourceContext.addGState(gState);
            codeBuffer.append("/").append(gState.getName()).append(" gs\n");
        }
    }

    private void establishDeviceCMYK(StringBuffer codeBuffer, Color color, boolean fill) {
        writeAlpha(color, codeBuffer);
        writeColor(codeBuffer, color, 4, (fill ? "k" : "K"));
    }

    private void writeColor(StringBuffer codeBuffer, Color color, int componentCount,
            String command) {
        float[] comps = color.getColorComponents(null);
        writeColor(codeBuffer, comps, componentCount, command);
    }

    private void writeColor(StringBuffer codeBuffer, float[] comps, int componentCount,
            String command) {
        if (comps.length != componentCount) {
            throw new IllegalStateException("Color with unexpected component count encountered");
        }
        for (float comp : comps) {
            DoubleFormatUtil.formatDouble(comp, 4, 4, codeBuffer);
            codeBuffer.append(" ");
        }
        codeBuffer.append(command).append("\n");
    }

}
