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

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.common.COSDictionaryMap;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDFontFactory;
import org.apache.pdfbox.pdmodel.graphics.PDExtendedGraphicsState;
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpaceFactory;
import org.apache.pdfbox.pdmodel.graphics.pattern.PDPatternResources;
import org.apache.pdfbox.pdmodel.graphics.shading.PDShadingResources;
import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObject;
import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObjectImage;
import org.apache.pdfbox.pdmodel.markedcontent.PDPropertyList;
import org.apache.pdfbox.util.MapUtil;

/**
 * This represents a set of resources available at the page/pages/stream level.
 * 
 * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
 * @author BM
 * 
 */
public class PDResources implements COSObjectable
{
    private COSDictionary resources;
    private Map<String, PDFont> fonts = null;
    private Map<PDFont, String> fontMappings = new HashMap<PDFont, String>();
    private Map<String, PDColorSpace> colorspaces = null;
    private Map<String, PDXObject> xobjects = null;
    private Map<PDXObject, String> xobjectMappings = null;
    private HashMap<String, PDXObjectImage> images = null;
    private Map<String, PDExtendedGraphicsState> graphicsStates = null;
    private Map<String, PDPatternResources> patterns = null;
    private Map<String, PDShadingResources> shadings = null;

    /**
     * Log instance.
     */
    private static final Log LOG = LogFactory.getLog(PDResources.class);

    /**
     * Default constructor.
     */
    public PDResources()
    {
        resources = new COSDictionary();
    }

    /**
     * Prepopulated resources.
     * 
     * @param resourceDictionary The cos dictionary for this resource.
     */
    public PDResources(COSDictionary resourceDictionary)
    {
        resources = resourceDictionary;
    }

    /**
     * This will get the underlying dictionary.
     * 
     * @return The dictionary for these resources.
     */
    public COSDictionary getCOSDictionary()
    {
        return resources;
    }

    /**
     * Convert this standard java object to a COS object.
     * 
     * @return The cos object that matches this Java object.
     */
    public COSBase getCOSObject()
    {
        return resources;
    }

    /**
     * Calling this will release all cached information.
     * 
     */
    public void clear()
    {
        if (fonts != null)
        {
            for(PDFont font : fonts.values())
            {
                font.clear();
            }
            fonts.clear();
            fonts = null;
        }
        if (fontMappings != null)
        {
            fontMappings.clear();
            fontMappings = null;
        }
        if (colorspaces != null)
        {
            colorspaces.clear();
            colorspaces = null;
        }
        if (xobjects != null)
        {
            for(PDXObject xobject : xobjects.values())
            {
                xobject.clear();
            }
            xobjects.clear();
            xobjects = null;
        }
        if (xobjectMappings != null)
        {
            xobjectMappings.clear();
            xobjectMappings = null;
        }
        if (images != null)
        {
            images.clear();
            images = null;
        }
        if (graphicsStates != null)
        {
            graphicsStates.clear();
            graphicsStates = null;
        }
        if (patterns != null)
        {
            patterns.clear();
            patterns = null;
        }
        if (shadings != null)
        {
            shadings.clear();
            shadings = null;
        }
    }

    /**
     * This will get the map of fonts. This will never return null. The keys are string and the values are PDFont
     * objects.
     * 
     * @param fontCache A map of existing PDFont objects to reuse.
     * @return The map of fonts.
     * 
     * @throws IOException If there is an error getting the fonts.
     * 
     * @deprecated due to some side effects font caching is no longer supported, use {@link #getFonts()} instead
     */
    public Map<String, PDFont> getFonts(Map<String, PDFont> fontCache) throws IOException
    {
        return getFonts();
    }

    /**
     * This will get the map of fonts. This will never return null.
     * 
     * @return The map of fonts.
     */
    public Map<String, PDFont> getFonts()
    {
        if (fonts == null)
        {
            // at least an empty map will be returned
            // TODO we should return null instead of an empty map
            fonts = new HashMap<String, PDFont>();
            COSDictionary fontsDictionary = (COSDictionary) resources.getDictionaryObject(COSName.FONT);
            if (fontsDictionary == null)
            {
                fontsDictionary = new COSDictionary();
                resources.setItem(COSName.FONT, fontsDictionary);
            }
            else
            {
                for (COSName fontName : fontsDictionary.keySet())
                {
                    COSBase font = fontsDictionary.getDictionaryObject(fontName);
                    // data-000174.pdf contains a font that is a COSArray, looks to be an error in the
                    // PDF, we will just ignore entries that are not dictionaries.
                    if (font instanceof COSDictionary)
                    {
                        PDFont newFont = null;
                        try
                        {
                            newFont = PDFontFactory.createFont((COSDictionary) font);
                        }
                        catch (IOException exception)
                        {
                            LOG.error("error while creating a font", exception);
                        }
                        if (newFont != null)
                        {
                            fonts.put(fontName.getName(), newFont);
                        }
                    }
                }
            }
            setFonts(fonts);
        }
        return fonts;
    }

    /**
     * This will get the map of PDXObjects that are in the resource dictionary. This will never return null.
     * 
     * @return The map of xobjects.
     */
    public Map<String, PDXObject> getXObjects()
    {
        if (xobjects == null)
        {
            // at least an empty map will be returned
            // TODO we should return null instead of an empty map
            xobjects = new HashMap<String, PDXObject>();
            COSDictionary xobjectsDictionary = (COSDictionary) resources.getDictionaryObject(COSName.XOBJECT);
            if (xobjectsDictionary == null)
            {
                xobjectsDictionary = new COSDictionary();
                resources.setItem(COSName.XOBJECT, xobjectsDictionary);
            }
            else
            {
                xobjects = new HashMap<String, PDXObject>();
                for (COSName objName : xobjectsDictionary.keySet())
                {
                    PDXObject xobject = null;
                    try
                    {
                        xobject = PDXObject.createXObject(xobjectsDictionary.getDictionaryObject(objName));
                    }
                    catch (IOException exception)
                    {
                        LOG.error("error while creating a xobject", exception);
                    }
                    if (xobject != null)
                    {
                        xobjects.put(objName.getName(), xobject);
                    }
                }
            }
            setXObjects(xobjects);
        }
        return xobjects;
    }

    /**
     * This will get the map of images. An empty map will be returned if there are no underlying images. So far the keys
     * are COSName of the image and the value is the corresponding PDXObjectImage.
     * 
     * @return The map of images.
     * @throws IOException If there is an error writing the picture.
     * 
     * @deprecated use {@link #getXObjects()} instead, as the images map isn't synchronized with the XObjects map.
     */
    public Map<String, PDXObjectImage> getImages() throws IOException
    {
        if (images == null)
        {
            Map<String, PDXObject> allXObjects = getXObjects();
            images = new HashMap<String, PDXObjectImage>();
            for (Map.Entry<String, PDXObject> entry : allXObjects.entrySet())
            {
                PDXObject xobject = entry.getValue();
                if (xobject instanceof PDXObjectImage)
                {
                    images.put(entry.getKey(), (PDXObjectImage) xobject);
                }
            }
        }
        return images;
    }

    /**
     * This will set the map of fonts.
     * 
     * @param fontsValue The new map of fonts.
     */
    public void setFonts(Map<String, PDFont> fontsValue)
    {
        fonts = fontsValue;
        if (fontsValue != null)
        {
            resources.setItem(COSName.FONT, COSDictionaryMap.convert(fontsValue));
            fontMappings = reverseMap(fontsValue, PDFont.class);
        }
        else
        {
            resources.removeItem(COSName.FONT);
            fontMappings = null;
        }
    }

    /**
     * This will set the map of xobjects.
     * 
     * @param xobjectsValue The new map of xobjects.
     */
    public void setXObjects(Map<String, PDXObject> xobjectsValue)
    {
        xobjects = xobjectsValue;
        if (xobjectsValue != null)
        {
            resources.setItem(COSName.XOBJECT, COSDictionaryMap.convert(xobjectsValue));
            xobjectMappings = reverseMap(xobjects, PDXObject.class);
        }
        else
        {
            resources.removeItem(COSName.XOBJECT);
            xobjectMappings = null;
        }
    }

    /**
     * This will get the map of colorspaces. This will return null if the underlying resources dictionary does not have
     * a colorspace dictionary. The keys are string and the values are PDColorSpace objects.
     * 
     * @return The map of colorspaces.
     */
    public Map<String, PDColorSpace> getColorSpaces()
    {
        if (colorspaces == null)
        {
            COSDictionary csDictionary = (COSDictionary) resources.getDictionaryObject(COSName.COLORSPACE);
            if (csDictionary != null)
            {
                colorspaces = new HashMap<String, PDColorSpace>();
                for (COSName csName : csDictionary.keySet())
                {
                    COSBase cs = csDictionary.getDictionaryObject(csName);
                    PDColorSpace colorspace = null;
                    try
                    {
                        colorspace = PDColorSpaceFactory.createColorSpace(cs);
                    }
                    catch (IOException exception)
                    {
                        LOG.error("error while creating a colorspace", exception);
                    }
                    if (colorspace != null)
                    {
                        colorspaces.put(csName.getName(), colorspace);
                    }
                }
            }
        }
        return colorspaces;
    }

    /**
     * This will set the map of colorspaces.
     * 
     * @param csValue The new map of colorspaces.
     */
    public void setColorSpaces(Map<String, PDColorSpace> csValue)
    {
        colorspaces = csValue;
        if (csValue != null)
        {
            resources.setItem(COSName.COLORSPACE, COSDictionaryMap.convert(csValue));
        }
        else
        {
            resources.removeItem(COSName.COLORSPACE);
        }
    }

    /**
     * This will get the map of graphic states. This will return null if the underlying resources dictionary does not
     * have a graphics dictionary. The keys are the graphic state name as a String and the values are
     * PDExtendedGraphicsState objects.
     * 
     * @return The map of extended graphic state objects.
     */
    public Map<String, PDExtendedGraphicsState> getGraphicsStates()
    {
        if (graphicsStates == null)
        {
            COSDictionary states = (COSDictionary) resources.getDictionaryObject(COSName.EXT_G_STATE);
            if (states != null)
            {
                graphicsStates = new HashMap<String, PDExtendedGraphicsState>();
                for (COSName name : states.keySet())
                {
                    COSDictionary dictionary = (COSDictionary) states.getDictionaryObject(name);
                    graphicsStates.put(name.getName(), new PDExtendedGraphicsState(dictionary));
                }
            }
        }
        return graphicsStates;
    }

    /**
     * This will set the map of graphics states.
     * 
     * @param states The new map of states.
     */
    public void setGraphicsStates(Map<String, PDExtendedGraphicsState> states)
    {
        graphicsStates = states;
        if (states != null)
        {
            Iterator<String> iter = states.keySet().iterator();
            COSDictionary dic = new COSDictionary();
            while (iter.hasNext())
            {
                String name = (String) iter.next();
                PDExtendedGraphicsState state = states.get(name);
                dic.setItem(COSName.getPDFName(name), state.getCOSObject());
            }
            resources.setItem(COSName.EXT_G_STATE, dic);
        }
        else
        {
            resources.removeItem(COSName.EXT_G_STATE);
        }
    }

    /**
     * Returns the dictionary mapping resource names to property list dictionaries for marked content.
     * 
     * @return the property list
     */
    public PDPropertyList getProperties()
    {
        PDPropertyList retval = null;
        COSDictionary props = (COSDictionary) resources.getDictionaryObject(COSName.PROPERTIES);

        if (props != null)
        {
            retval = new PDPropertyList(props);
        }
        return retval;
    }

    /**
     * Sets the dictionary mapping resource names to property list dictionaries for marked content.
     * 
     * @param props the property list
     */
    public void setProperties(PDPropertyList props)
    {
        resources.setItem(COSName.PROPERTIES, props.getCOSObject());
    }

    /**
     * This will get the map of patterns. This will return null if the underlying resources dictionary does not have a
     * patterns dictionary. The keys are the pattern name as a String and the values are PDPatternResources objects.
     * 
     * @return The map of pattern resources objects.
     * 
     * @throws IOException If there is an error getting the pattern resources.
     */
    public Map<String, PDPatternResources> getPatterns() throws IOException
    {
        if (patterns == null)
        {
            COSDictionary patternsDictionary = (COSDictionary) resources.getDictionaryObject(COSName.PATTERN);
            if (patternsDictionary != null)
            {
                patterns = new HashMap<String, PDPatternResources>();
                for (COSName name : patternsDictionary.keySet())
                {
                    COSDictionary dictionary = (COSDictionary) patternsDictionary.getDictionaryObject(name);
                    patterns.put(name.getName(), PDPatternResources.create(dictionary));
                }
            }
        }
        return patterns;
    }

    /**
     * This will set the map of patterns.
     * 
     * @param patternsValue The new map of patterns.
     */
    public void setPatterns(Map<String, PDPatternResources> patternsValue)
    {
        patterns = patternsValue;
        if (patternsValue != null)
        {
            Iterator<String> iter = patternsValue.keySet().iterator();
            COSDictionary dic = new COSDictionary();
            while (iter.hasNext())
            {
                String name = iter.next();
                PDPatternResources pattern = patternsValue.get(name);
                dic.setItem(COSName.getPDFName(name), pattern.getCOSObject());
            }
            resources.setItem(COSName.PATTERN, dic);
        }
        else
        {
            resources.removeItem(COSName.PATTERN);
        }
    }

    /**
     * This will get the map of shadings. This will return null if the underlying resources dictionary does not have a
     * shading dictionary. The keys are the shading name as a String and the values are PDShadingResources objects.
     * 
     * @return The map of shading resources objects.
     * 
     * @throws IOException If there is an error getting the shading resources.
     */
    public Map<String, PDShadingResources> getShadings() throws IOException
    {
        if (shadings == null)
        {
            COSDictionary shadingsDictionary = (COSDictionary) resources.getDictionaryObject(COSName.SHADING);
            if (shadingsDictionary != null)
            {
                shadings = new HashMap<String, PDShadingResources>();
                for (COSName name : shadingsDictionary.keySet())
                {
                    COSDictionary dictionary = (COSDictionary) shadingsDictionary.getDictionaryObject(name);
                    shadings.put(name.getName(), PDShadingResources.create(dictionary));
                }
            }
        }
        return shadings;
    }

    /**
     * This will set the map of shadings.
     * 
     * @param shadingsValue The new map of shadings.
     */
    public void setShadings(Map<String, PDShadingResources> shadingsValue)
    {
        shadings = shadingsValue;
        if (shadingsValue != null)
        {
            Iterator<String> iter = shadingsValue.keySet().iterator();
            COSDictionary dic = new COSDictionary();
            while (iter.hasNext())
            {
                String name = iter.next();
                PDShadingResources shading = shadingsValue.get(name);
                dic.setItem(COSName.getPDFName(name), shading.getCOSObject());
            }
            resources.setItem(COSName.SHADING, dic);
        }
        else
        {
            resources.removeItem(COSName.SHADING);
        }
    }

    /**
     * Adds the given font to the resources of the current page.
     * 
     * @param font the font to be added
     * @return the font name to be used within the content stream.
     */
    public String addFont(PDFont font)
    {
        // use the getter to initialize a possible empty fonts map
        return addFont(font, MapUtil.getNextUniqueKey(getFonts(), "F"));
    }

    /**
     * Adds the given font to the resources of the current page using the given font key.
     * 
     * @param font the font to be added
     * @param fontKey key to used to map to the given font
     * @return the font name to be used within the content stream.
     */
    public String addFont(PDFont font, String fontKey)
    {
        if (fonts == null)
        {
            // initialize fonts map
            getFonts();
        }

        String fontMapping = fontMappings.get(font);
        if (fontMapping == null)
        {
            fontMapping = fontKey;
            fontMappings.put(font, fontMapping);
            fonts.put(fontMapping, font);
            addFontToDictionary(font, fontMapping);
        }
        return fontMapping;
    }

    private void addFontToDictionary(PDFont font, String fontName)
    {
        COSDictionary fontsDictionary = (COSDictionary) resources.getDictionaryObject(COSName.FONT);
        fontsDictionary.setItem(fontName, font);
    }

    /**
     * Adds the given XObject to the resources of the current the page.
     * 
     * @param xobject the XObject to be added
     * @param prefix the prefix to be used for the name
     * 
     * @return the XObject name to be used within the content stream.
     */
    public String addXObject(PDXObject xobject, String prefix)
    {
        if (xobjects == null)
        {
            // initialize XObject map
            getXObjects();
        }
        String objMapping = xobjectMappings.get(xobject);
        if (objMapping == null)
        {
            objMapping = MapUtil.getNextUniqueKey(xobjects, prefix);
            xobjectMappings.put(xobject, objMapping);
            xobjects.put(objMapping, xobject);
            addXObjectToDictionary(xobject, objMapping);
        }
        return objMapping;
    }

    private void addXObjectToDictionary(PDXObject xobject, String xobjectName)
    {
        COSDictionary xobjectsDictionary = (COSDictionary) resources.getDictionaryObject(COSName.XOBJECT);
        xobjectsDictionary.setItem(xobjectName, xobject);
    }

    private <T> Map<T, String> reverseMap(Map<String, T> map, Class<T> keyClass)
    {
        Map<T, String> reversed = new java.util.HashMap<T, String>();
        for (Map.Entry<String, T> entry : map.entrySet())
        {
            reversed.put(keyClass.cast(entry.getValue()), (String) entry.getKey());
        }
        return reversed;
    }

}
