/*

   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.batik.svggen;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.Collections;

import org.apache.batik.ext.awt.g2d.GraphicContext;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * This class is used by the SVGGraphics2D SVG Generator to manage
 * addition of new Nodes to the SVG DOM Tree. This class handles
 * a set of DOMGroupManager objects that can all append to the
 * top level group managed by this class. This allows multiple
 * SVGGraphics2D instances, created from the same SVGGraphics2D
 * through the various create methods, to append to the same
 * SVG document and keep the rendering order correct.
 *
 * The root node managed by this DOMTreeManager contains two children:
 * a top level group node and a top level defs node. The top level
 * defs node contains the definition of common SVG entities such as
 * the various AlphaComposite rules. Note that other defs can also be
 * created under the top level group, for example to represent
 * gradient or pattern paints.
 * <br>
 * [svg]
 *   |
 *   +-- [defs] Contain generic definitions
 *   +-- [g]    Top level group
 *        |
 *        +-- [defs] Contains definitions specific to rendering
 *        +-- [g]    Group 1
 *        +-- ...
 *        +-- [g]    Group n
 *
 * @author <a href="mailto:cjolif">Christophe Jolif</a>
 * @author <a href="mailto:vincent.hardy@eng.sun.com">Vincent Hardy</a>
 * @version $Id: DOMTreeManager.java 1805408 2017-08-18 12:21:52Z ssteiner $
 */
public class DOMTreeManager implements SVGSyntax, ErrorConstants {

    /**
     * Maximum of Graphic Context attributes overrides
     * in children of the current group.
     */
    int maxGCOverrides;

    /**
     * Set of group managers that build groups for
     * this manager.
     * The synchronizedList is part of the fix for bug #40686
     */
    protected final List groupManagers = Collections.synchronizedList( new ArrayList() );

    /**
     * Set of definitions that are to be placed at the top of the
     * document tree
     */
    protected List genericDefSet = new LinkedList();

    /**
     * Default SVG GraphicContext state
     */
    SVGGraphicContext defaultGC;

    /**
     * Top level group
     */
    protected Element topLevelGroup;

    /**
     * Used to convert the Java 2D API graphic context state
     * into the SVG equivalent set of attributes and related
     * definitions
     */
    SVGGraphicContextConverter gcConverter;

    /**
     * The context that stores the domFactory, the imageHandler
     * and the extensionHandler.
     */
    protected SVGGeneratorContext generatorContext;

    /**
     * Converters used bVy this object to translate graphic context
     * attributes
     */
    protected SVGBufferedImageOp filterConverter;

    /**
     * Set of definitions which can be used by custom extensions
     */
    protected List otherDefs;

    /**
     * Constructor
     * @param gc default graphic context state
     * @param generatorContext the SVG generator context
     * @param maxGCOverrides defines how many overrides are allowed
     *                       in children nodes of the current group.
     */
    public DOMTreeManager(GraphicContext gc,
                          SVGGeneratorContext generatorContext,
                          int maxGCOverrides){
        if (gc == null)
            throw new SVGGraphics2DRuntimeException(ERR_GC_NULL);

        if (maxGCOverrides <= 0)
            throw new SVGGraphics2DRuntimeException(ERR_MAXGCOVERRIDES_OUTOFRANGE);

        if (generatorContext == null)
            throw new SVGGraphics2DRuntimeException(ERR_CONTEXT_NULL);

        this.generatorContext = generatorContext;
        this.maxGCOverrides = maxGCOverrides;

        // Start with a new Top Level Group
        recycleTopLevelGroup();

        // Build the default GC descriptor
        defaultGC = gcConverter.toSVG(gc);
    }

    /**
     * @param groupManager new DOMGroupManager to add to the list of
     *        managers that collaborate with this tree manager.
     */
    public void addGroupManager(DOMGroupManager groupManager){
        if(groupManager != null)
            groupManagers.add(groupManager);
    }

    /**
     * @param groupManager DOMGroupManager to remove from the list of
     *        managers that collaborate with this tree manager
     */
    public void removeGroupManager(DOMGroupManager groupManager){
        if(groupManager != null)
            groupManagers.remove( groupManager );
    }

    /**
     * When a group is appended to the tree by this call, all the
     * other group managers are requested to start new groups, in
     * order to preserve the Z-order.
     *
     * @param group new group to be appended to the topLevelGroup
     * @param groupManager DOMTreeManager that produced the group.
     */
    public void appendGroup(Element group, DOMGroupManager groupManager){
        topLevelGroup.appendChild(group);
        synchronized( groupManagers ){
            // we want to prevent that the groupManagers-list changes while
            // we iterate over it. If that would happen, we might skip entries
            // within the list or ignore new entries at the end. Fix #40686
            int nManagers = groupManagers.size();
            for (Object groupManager1 : groupManagers) {
                DOMGroupManager gm = (DOMGroupManager) groupManager1;
                if (gm != groupManager)
                    gm.recycleCurrentGroup();
            }
        }
    }

    /**
     * Reset the state of this object to handler a new topLevelGroup
     */
    protected void recycleTopLevelGroup(){
        recycleTopLevelGroup(true);
    }


    /**
     * Reset the state of this object to handler a new topLevelGroup
     */
    protected void recycleTopLevelGroup(boolean recycleConverters){
        // First, recycle group managers
        synchronized( groupManagers ){
            // we want to prevent that the groupManagers-list changes while
            // we iterate over it. If that would happen, we might skip entries
            // within the list or ignore new entries at the end. Fix #40686
            int nManagers = groupManagers.size();
            for (Object groupManager : groupManagers) {
                DOMGroupManager gm = (DOMGroupManager) groupManager;
                gm.recycleCurrentGroup();
            }
        }

        // Create top level group node
        topLevelGroup = generatorContext.domFactory.
            createElementNS(SVG_NAMESPACE_URI, SVG_G_TAG);

        // Build new converters
        if (recycleConverters) {
            filterConverter =
                new SVGBufferedImageOp(generatorContext);
            gcConverter =
                new SVGGraphicContextConverter(generatorContext);
        }
    }

    /**
     * Sets the topLevelGroup to the input element. This will throw an
     * exception if the input element is not of type 'g' or if it is
     * null.
     */
    public void setTopLevelGroup(Element topLevelGroup){
        if(topLevelGroup == null)
            throw new SVGGraphics2DRuntimeException(ERR_TOP_LEVEL_GROUP_NULL);

        if(!SVG_G_TAG.equalsIgnoreCase(topLevelGroup.getTagName()))
            throw new SVGGraphics2DRuntimeException(ERR_TOP_LEVEL_GROUP_NOT_G);

        recycleTopLevelGroup(false);
        this.topLevelGroup = topLevelGroup;
    }

    /**
     * Returns the root element with the generic definitions and
     * the topLevelGroup.
     */
    public Element getRoot(){
        return getRoot(null);
    }

    /**
     * Returns the root element with the generic definitions and
     * the topLevelGroup.
     */
    public Element getRoot(Element svgElement){
        Element svg = svgElement;

        if (svg == null) {
            svg = generatorContext.domFactory.
                createElementNS(SVG_NAMESPACE_URI, SVG_SVG_TAG);
        }

        // Enable background if required by AlphaComposite convertion
        if (gcConverter.getCompositeConverter().
            getAlphaCompositeConverter().requiresBackgroundAccess())
            svg.setAttributeNS
                (null, SVG_ENABLE_BACKGROUND_ATTRIBUTE, SVG_NEW_VALUE);

        if (generatorContext.generatorComment != null) {
            Comment generatorComment = generatorContext.domFactory.
                createComment(generatorContext.generatorComment);
            svg.appendChild(generatorComment);
        }

        // Set default rendering context attributes in node
        applyDefaultRenderingStyle(svg);

        svg.appendChild(getGenericDefinitions());
        svg.appendChild(getTopLevelGroup());

        return svg;
    }

    public void applyDefaultRenderingStyle(Element element) {
        Map groupDefaults = defaultGC.getGroupContext();
        generatorContext.styleHandler.setStyle(element, groupDefaults, generatorContext);
    }

    /**
     * @return a defs element that contains all the generic
     *         definitions
     */
    public Element getGenericDefinitions() {
        // when called several times, this will create several generic
        // definition elements... not sure it is desired behavior...
        Element genericDefs =
            generatorContext.domFactory.createElementNS(SVG_NAMESPACE_URI,
                                                        SVG_DEFS_TAG);
        for (Object aGenericDefSet : genericDefSet) {
            genericDefs.appendChild((Element) aGenericDefSet);
        }

        genericDefs.setAttributeNS(null, SVG_ID_ATTRIBUTE, ID_PREFIX_GENERIC_DEFS);
        return genericDefs;
    }

    /**
     * @return the extension handler used by the DOMTreeManager.
     */
    public ExtensionHandler getExtensionHandler(){
        return generatorContext.getExtensionHandler();
    }

    /**
     * This will change the extension handler on the
     * <code>SVGGeneratorContext</code>.
     * @param extensionHandler new extension handler this object should use
     */
    void setExtensionHandler(ExtensionHandler extensionHandler) {
        generatorContext.setExtensionHandler(extensionHandler);
    }

    /**
     * Invoking this method will return a set of definition element that
     * contain all the definitions referenced by the attributes generated by
     * the various converters. This also resets the converters.
     */
    public List getDefinitionSet(){
        //
        // The definition set contains all the definitions minus
        // any definition that has been placed in the generic definition set
        //
        List defSet = gcConverter.getDefinitionSet();
        defSet.removeAll(genericDefSet);
        defSet.addAll(filterConverter.getDefinitionSet());
        if (otherDefs != null){
            defSet.addAll(otherDefs);
            otherDefs = null;
        }

        // Build new converters
        filterConverter = new SVGBufferedImageOp(generatorContext);
        gcConverter = new SVGGraphicContextConverter(generatorContext);

        return defSet;
    }

    /**
     * Lets custom implementations for various extensions add
     * elements to the &lt;defs&gt; sections.
     */
    public void addOtherDef(Element definition){
        if (otherDefs == null){
            otherDefs = new LinkedList();
        }

        otherDefs.add(definition);
    }

    /**
     * Invoking this method will return a reference to the topLevelGroup
     * Element managed by this object. It will also cause this object
     * to start working with a new topLevelGroup.
     *
     * @return top level group
     */
    public Element getTopLevelGroup(){
        boolean includeDefinitionSet = true;
        return getTopLevelGroup(includeDefinitionSet);
    }

    /**
     * Invoking this method will return a reference to the topLevelGroup
     * Element managed by this object. It will also cause this object
     * to start working with a new topLevelGroup.
     *
     * @param includeDefinitionSet if true, the definition set is included and
     *        the converters are reset (i.e., they start with an empty set
     *        of definitions).
     * @return top level group
     */
    public Element getTopLevelGroup(boolean includeDefinitionSet){
        Element topLevelGroup = this.topLevelGroup;

        //
        // Include definition set if requested
        //
        if(includeDefinitionSet){
            List defSet = getDefinitionSet();
            if(defSet.size() > 0){
                Element defElement = null;

                NodeList defsElements =
                    topLevelGroup.getElementsByTagName(SVG_DEFS_TAG);
                if (defsElements.getLength() > 0)
                    defElement = (Element)defsElements.item(0);

                if (defElement == null) {
                    defElement =
                        generatorContext.domFactory.
                        createElementNS(SVG_NAMESPACE_URI,
                                        SVG_DEFS_TAG);
                    defElement.
                        setAttributeNS(null, SVG_ID_ATTRIBUTE,
                                       generatorContext.idGenerator.
                                       generateID(ID_PREFIX_DEFS));
                    topLevelGroup.insertBefore(defElement,
                                               topLevelGroup.getFirstChild());
                }

                for (Object aDefSet : defSet) defElement.appendChild((Element) aDefSet);
            }
        }

        // If the definition set is included, then the converters have already
        // been recycled in getDefinitionSet. Otherwise, they should not be
        // recycled. So, in all cases, do not recycle the converters here.
        recycleTopLevelGroup(false);

        return topLevelGroup;
    }

    public SVGBufferedImageOp getFilterConverter() {
        return filterConverter;
    }

    public SVGGraphicContextConverter getGraphicContextConverter() {
        return gcConverter;
    }

    SVGGeneratorContext getGeneratorContext() {
        return generatorContext;
    }

    Document getDOMFactory() {
        return generatorContext.domFactory;
    }

    StyleHandler getStyleHandler() {
        return generatorContext.styleHandler;
    }
}
