/*
 * 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: PDFResources.java 1843494 2018-10-10 19:21:55Z matthias $ */

package org.apache.fop.pdf;

import java.io.IOException;
import java.io.OutputStream;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.apache.xmlgraphics.java2d.color.profile.ColorProfileUtil;

import org.apache.fop.fonts.FontDescriptor;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.fonts.Typeface;
import org.apache.fop.fonts.base14.Symbol;
import org.apache.fop.fonts.base14.ZapfDingbats;

/**
 * Class representing a /Resources object.
 *
 * /Resources object contain a list of references to the fonts, patterns,
 * shadings, etc.,  for the document.
 */
public class PDFResources extends PDFDictionary {

    /**
     * /Font objects keyed by their internal name
     */
    protected Map<String, PDFDictionary> fonts = new LinkedHashMap<String, PDFDictionary>();

    /**
     * Set of XObjects
     */
    protected Set<PDFXObject> xObjects = new LinkedHashSet<PDFXObject>();
    /** Map of color spaces (key: color space name) */
    protected Map<LazyName, PDFColorSpace> colorSpaces = new LinkedHashMap<LazyName, PDFColorSpace>();

    /** Map of ICC color spaces (key: ICC profile description) */
    protected Map<String, PDFICCBasedColorSpace> iccColorSpaces = new LinkedHashMap<String, PDFICCBasedColorSpace>();

    private PDFResources parent;
    private PDFDictionary fontsObj;
    private Map<String, PDFDictionary> fontsObjDict = new LinkedHashMap<String, PDFDictionary>();

    /** Named properties */
    protected Map<String, PDFReference> properties = new LinkedHashMap<String, PDFReference>();

    protected Set<PDFResourceContext> contexts = new LinkedHashSet<PDFResourceContext>();

    /**
     * create a /Resources object.
     */
    public PDFResources(PDFDocument doc) {
        /* generic creation of object */
        super();
        setObjectNumber(doc);
    }

    public void addContext(PDFResourceContext c) {
        contexts.add(c);
    }

    public void setParentResources(PDFResources p) {
        parent = p;
    }

    public PDFResources getParentResources() {
        return parent;
    }

    /**
     * add font object to resources list.
     *
     * @param font the PDFFont to add
     */
    public void addFont(PDFFont font) {
        addFont(font.getName(), font);
    }

    public void addFont(String name, PDFDictionary font) {
        if (fontsObj != null) {
            fontsObj.put(name, font);
            fontsObjDict.put(name, font);
        } else {
            fonts.put(name, font);
        }
    }

    public void createFontsAsObj() {
        fontsObj = new PDFDictionary();
        getDocument().registerTrailerObject(fontsObj);
        put("Font", fontsObj);
    }

    /**
     * Add the fonts in the font info to this PDF document's Font Resources.
     *
     * @param doc PDF document to add fonts to
     * @param fontInfo font info object to get font information from
     */
   public void addFonts(PDFDocument doc, FontInfo fontInfo) {
        Map<String, Typeface> usedFonts = fontInfo.getUsedFonts();
        for (Map.Entry<String, Typeface> e : usedFonts.entrySet()) {
            String f = e.getKey();
            Typeface font = e.getValue();

            //Check if the font actually had any mapping operations. If not, it is an indication
            //that it has never actually been used and therefore doesn't have to be embedded.
            if (font.hadMappingOperations()) {
                FontDescriptor desc = null;
                if (font instanceof FontDescriptor) {
                    desc = (FontDescriptor)font;
                }
                String encoding = font.getEncodingName();
                if (font instanceof Symbol || font instanceof ZapfDingbats) {
                    encoding = null; //Symbolic fonts shouldn't specify an encoding value in PDF
                }
                addFont(doc.getFactory().makeFont(
                    f, font.getEmbedFontName(), encoding, font, desc));
            }
        }
   }

    /**
     * Add an XObject to the resources.
     *
     * @param xObject the XObject to add
     */
    public void addXObject(PDFXObject xObject) {
        this.xObjects.add(xObject);
    }

    /**
     * Add a ColorSpace dictionary to the resources.
     * @param colorSpace the color space
     */
    public void addColorSpace(PDFColorSpace colorSpace) {
        this.colorSpaces.put(new LazyName(colorSpace), colorSpace);
        if (colorSpace instanceof PDFICCBasedColorSpace) {
            PDFICCBasedColorSpace icc = (PDFICCBasedColorSpace)colorSpace;
            String desc = ColorProfileUtil.getICCProfileDescription(
                    icc.getICCStream().getICCProfile());
            this.iccColorSpaces.put(desc, icc);
        }
    }

    static class LazyName {
        private PDFColorSpace colorSpace;
        public LazyName(PDFColorSpace colorSpace) {
            this.colorSpace = colorSpace;
        }
        public PDFName getName() {
            return new PDFName(colorSpace.getName());
        }
    }

    /**
     * Returns a ICCBased color space by profile name.
     * @param desc the name of the color space
     * @return the requested color space or null if it wasn't found
     */
    public PDFICCBasedColorSpace getICCColorSpaceByProfileName(String desc) {
        PDFICCBasedColorSpace cs = this.iccColorSpaces.get(desc);
        return cs;
    }

    /**
     * Returns a color space by name.
     * @param name the name of the color space
     * @return the requested color space or null if it wasn't found
     */
    public PDFColorSpace getColorSpace(PDFName name) {
        for (Map.Entry<LazyName, PDFColorSpace> x : colorSpaces.entrySet()) {
            if (x.getKey().getName().equals(name)) {
                return x.getValue();
            }
        }
        return null;
    }

    /**
     * Add a named property.
     *
     * @param name name of property
     * @param property reference to property value
     */
    public void addProperty(String name, PDFReference property) {
        this.properties.put(name, property);
    }

    /**
     * Get a named property.
     *
     * @param name name of property
     */
    public PDFReference getProperty(String name) {
        return this.properties.get(name);
    }

    @Override
    public int output(OutputStream stream) throws IOException {
        populateDictionary();
        return super.output(stream);
    }

    private void populateDictionary() {
        if (parent != null && parent.fontsObj != null) {
            put("Font", parent.fontsObj);
        }
        if (!this.fonts.isEmpty() || (parent != null && !parent.fonts.isEmpty())) {
            PDFDictionary dict = new PDFDictionary(this);
            /* construct PDF dictionary of font object references */
            for (Map.Entry<String, PDFDictionary> entry : fonts.entrySet()) {
                dict.put(entry.getKey(), entry.getValue());
            }
            if (parent != null) {
                for (Map.Entry<String, PDFDictionary> entry : parent.fonts.entrySet()) {
                    dict.put(entry.getKey(), entry.getValue());
                }
                for (Map.Entry<String, PDFDictionary> entry : parent.fontsObjDict.entrySet()) {
                    dict.put(entry.getKey(), entry.getValue());
                }
            }
            put("Font", dict);
        }

        Set<PDFPattern> patterns = new LinkedHashSet<PDFPattern>();
        Set<PDFShading> shadings = new LinkedHashSet<PDFShading>();
        Set<PDFGState> gstates = new LinkedHashSet<PDFGState>();
        for (PDFResourceContext c : contexts) {
            xObjects.addAll(c.getXObjects());
            patterns.addAll(c.getPatterns());
            shadings.addAll(c.getShadings());
            gstates.addAll(c.getGStates());
        }
        if (parent != null) {
            xObjects.addAll(parent.xObjects);
            for (PDFResourceContext c : parent.contexts) {
                patterns.addAll(c.getPatterns());
                shadings.addAll(c.getShadings());
                gstates.addAll(c.getGStates());
            }
        }

        if (!shadings.isEmpty()) {
            PDFDictionary dict = (PDFDictionary) get("Shading");
            if (dict == null) {
                dict = new PDFDictionary(this);
            }
            for (PDFShading shading : shadings) {
                dict.put(shading.getName(), shading);
            }
            put("Shading", dict);
        }

        if (!patterns.isEmpty()) {
            PDFDictionary dict = (PDFDictionary) get("Pattern");
            if (dict == null) {
                dict = new PDFDictionary(this);
            }
            for (PDFPattern pattern : patterns) {
                dict.put(pattern.getName(), pattern);
            }
            put("Pattern", dict);
        }

        PDFArray procset = new PDFArray(this);
        procset.add(new PDFName("PDF"));
        procset.add(new PDFName("ImageB"));
        procset.add(new PDFName("ImageC"));
        procset.add(new PDFName("Text"));
        put("ProcSet", procset);

        if (!xObjects.isEmpty()) {
            PDFDictionary dict = (PDFDictionary) get("XObject");
            if (dict == null) {
                dict = new PDFDictionary(this);
            }
            for (PDFXObject xObject : xObjects) {
                dict.put(xObject.getName().toString(), xObject);
            }
            put("XObject", dict);
        }

        if (!gstates.isEmpty()) {
            PDFDictionary dict = (PDFDictionary) get("ExtGState");
            if (dict == null) {
                dict = new PDFDictionary(this);
            }
            for (PDFGState gstate : gstates) {
                dict.put(gstate.getName(), gstate);
            }
            put("ExtGState", dict);
        }

        if (!this.colorSpaces.isEmpty() || (parent != null && !parent.colorSpaces.isEmpty())) {
            PDFDictionary dict = (PDFDictionary)this.get("ColorSpace");
            if (dict == null) {
                dict = new PDFDictionary(this);
            }
            if (parent != null) {
                for (PDFColorSpace colorSpace : parent.colorSpaces.values()) {
                    dict.put(colorSpace.getName(), colorSpace);
                }
            }
            for (PDFColorSpace colorSpace : colorSpaces.values()) {
                dict.put(colorSpace.getName(), colorSpace);
            }
            put("ColorSpace", dict);
        }

        if (!properties.isEmpty()) {
            PDFDictionary dict = new PDFDictionary(this);
            for (Map.Entry<String, PDFReference> stringPDFReferenceEntry : properties.entrySet()) {
                dict.put(stringPDFReferenceEntry.getKey(), stringPDFReferenceEntry.getValue());
            }
            put("Properties", dict);
        }
    }

    @Override
    public void getChildren(Set<PDFObject> children) {
        getChildren(children, false);
    }

    private void getChildren(Set<PDFObject> children, boolean isParent) {
        super.getChildren(children);
        for (PDFDictionary f : fonts.values()) {
            children.add(f);
            f.getChildren(children);
        }
        for (PDFResourceContext c : contexts) {
            for (PDFXObject x : c.getXObjects()) {
                children.add(x);
                x.getChildren(children);
            }
            for (PDFPattern x : c.getPatterns()) {
                children.add(x);
                x.getChildren(children);
            }
            for (PDFShading x : c.getShadings()) {
                children.add(x);
                x.getChildren(children);
            }
            for (PDFGState x : c.getGStates()) {
                children.add(x);
                x.getChildren(children);
            }
        }
        if (!isParent) {
            for (PDFColorSpace x : colorSpaces.values()) {
                children.add((PDFObject)x);
                ((PDFObject)x).getChildren(children);
            }
        }
        if (parent != null) {
            parent.getChildren(children, true);
        }
    }
}
