/*****************************************************************************
 * Copyright (C) The Apache Software Foundation. All rights reserved.        *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included with this distribution in  *
 * the LICENSE file.                                                         *
 *****************************************************************************/

package org.apache.batik.bridge;

import java.awt.Color;
import java.awt.Paint;
import java.awt.geom.AffineTransform;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.batik.dom.svg.SVGOMDocument;
import org.apache.batik.dom.util.XLinkSupport;
import org.apache.batik.ext.awt.MultipleGradientPaint;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.util.ParsedURL;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * Bridge class for vending gradients.
 *
 * @author <a href="mailto:tkormann@apache.org">Thierry Kormann</a>
 * @version $Id: AbstractSVGGradientElementBridge.java,v 1.10 2003/04/11 13:54:41 vhardy Exp $
 */
public abstract class AbstractSVGGradientElementBridge extends AbstractSVGBridge
    implements PaintBridge, ErrorConstants {

    /**
     * Constructs a new AbstractSVGGradientElementBridge.
     */
    protected AbstractSVGGradientElementBridge() {}

    /**
     * Creates a <tt>Paint</tt> according to the specified parameters.
     *
     * @param ctx the bridge context to use
     * @param paintElement the element that defines a Paint
     * @param paintedElement the element referencing the paint
     * @param paintedNode the graphics node on which the Paint will be applied
     * @param opacity the opacity of the Paint to create
     */
    public Paint createPaint(BridgeContext ctx,
                             Element paintElement,
                             Element paintedElement,
                             GraphicsNode paintedNode,
                             float opacity) {

        String s;

        // stop elements
        List stops = extractStop(paintElement, opacity, ctx);
        // if no stops are defined, painting is the same as 'none'
        if (stops == null) {
            return null;
        }
        int stopLength = stops.size();
        // if one stops is defined, painting is the same as a single color
        if (stopLength == 1) {
            return ((Stop)stops.get(0)).color;
        }
        float [] offsets = new float[stopLength];
        Color [] colors = new Color[stopLength];
        Iterator iter = stops.iterator();
        for (int i=0; iter.hasNext(); ++i) {
            Stop stop = (Stop)iter.next();
            offsets[i] = stop.offset;
            colors[i] = stop.color;
        }

        // 'spreadMethod' attribute - default is pad
        MultipleGradientPaint.CycleMethodEnum spreadMethod
            = MultipleGradientPaint.NO_CYCLE;
        s = SVGUtilities.getChainableAttributeNS
            (paintElement, null, SVG_SPREAD_METHOD_ATTRIBUTE, ctx);
        if (s.length() != 0) {
            spreadMethod = convertSpreadMethod(paintElement, s);
        }

        // 'color-interpolation' CSS property
        MultipleGradientPaint.ColorSpaceEnum colorSpace
            = CSSUtilities.convertColorInterpolation(paintElement);

        // 'gradientTransform' attribute - default is an Identity matrix
        AffineTransform transform;
        s = SVGUtilities.getChainableAttributeNS
            (paintElement, null, SVG_GRADIENT_TRANSFORM_ATTRIBUTE, ctx);
        if (s.length() != 0) {
            transform = SVGUtilities.convertTransform
                (paintElement, SVG_GRADIENT_TRANSFORM_ATTRIBUTE, s);
        } else {
            transform = new AffineTransform();
        }

        Paint paint = buildGradient(paintElement,
                                    paintedElement,
                                    paintedNode,
                                    spreadMethod,
                                    colorSpace,
                                    transform,
                                    colors,
                                    offsets,
                                    ctx);
        return paint;
    }

    /**
     * Builds a concrete gradient according to the specified parameters.
     *
     * @param paintElement the element that defines a Paint
     * @param paintedElement the element referencing the paint
     * @param paintedNode the graphics node on which the Paint will be applied
     * @param spreadMethod the spread method
     * @param colorSpace the color space (sRGB | LinearRGB)
     * @param transform the gradient transform
     * @param colors the colors of the gradient
     * @param offsets the offsets
     * @param ctx the bridge context to use
     */
    protected abstract
        Paint buildGradient(Element paintElement,
                            Element paintedElement,
                            GraphicsNode paintedNode,
                            MultipleGradientPaint.CycleMethodEnum spreadMethod,
                            MultipleGradientPaint.ColorSpaceEnum colorSpace,
                            AffineTransform transform,
                            Color [] colors,
                            float [] offsets,
                            BridgeContext ctx);

    // convenient methods

    /**
     * Converts the spreadMethod attribute.
     *
     * @param paintElement the paint Element with a spreadMethod
     * @param s the spread method
     */
    protected static MultipleGradientPaint.CycleMethodEnum convertSpreadMethod
        (Element paintElement, String s) {
        if (SVG_REPEAT_VALUE.equals(s)) {
            return MultipleGradientPaint.REPEAT;
        }
        if (SVG_REFLECT_VALUE.equals(s)) {
            return MultipleGradientPaint.REFLECT;
        }
        if (SVG_PAD_VALUE.equals(s)) {
            return MultipleGradientPaint.NO_CYCLE;
        }
        throw new BridgeException
            (paintElement, ERR_ATTRIBUTE_VALUE_MALFORMED,
             new Object[] {SVG_SPREAD_METHOD_ATTRIBUTE, s});
    }

    /**
     * Returns the stops elements of the specified gradient
     * element. Stops can be children of the gradients or defined on
     * one of its 'ancestor' (linked with the xlink:href attribute).
     *
     * @param paintElement the gradient element
     * @param opacity the opacity
     * @param ctx the bridge context to use
     */
    protected static List extractStop(Element paintElement,
                                      float opacity,
                                      BridgeContext ctx) {

        List refs = new LinkedList();
        for (;;) {
            List stops = extractLocalStop(paintElement, opacity, ctx);
            if (stops != null) {
                return stops; // stop elements found, exit
            }
            String uri = XLinkSupport.getXLinkHref(paintElement);
            if (uri.length() == 0) {
                return null; // no xlink:href found, exit
            }
            // check if there is circular dependencies
            SVGOMDocument doc = (SVGOMDocument)paintElement.getOwnerDocument();
            ParsedURL purl = new ParsedURL(doc.getURL(), uri);
            if (!purl.complete())
                throw new BridgeException(paintElement,
                                          ERR_URI_MALFORMED,
                                          new Object[] {uri});

            if (contains(refs, purl)) {
                throw new BridgeException(paintElement,
                                          ERR_XLINK_HREF_CIRCULAR_DEPENDENCIES,
                                          new Object[] {uri});
            }
            refs.add(purl);
            paintElement = ctx.getReferencedElement(paintElement, uri);
        }
    }

    /**
     * Returns a list of <tt>Stop</tt> elements, children of the
     * specified paintElement can have or null if any.
     *
     * @param gradientElement the paint element
     * @param opacity the opacity
     * @param ctx the bridge context
     */
    protected static List extractLocalStop(Element gradientElement,
                                           float opacity,
                                           BridgeContext ctx) {
        LinkedList stops = null;
        Stop previous = null;
        for (Node n = gradientElement.getFirstChild();
             n != null;
             n = n.getNextSibling()) {

            if ((n.getNodeType() != Node.ELEMENT_NODE)) {
                continue;
            }

            Element e = (Element)n;
            Bridge bridge = ctx.getBridge(e);
            if (bridge == null || !(bridge instanceof SVGStopElementBridge)) {
                continue;
            }
            Stop stop = ((SVGStopElementBridge)bridge).createStop
                (ctx, gradientElement, e, opacity);
            if (stops == null) {
                stops = new LinkedList();
            }
            if (previous != null) {
                if (stop.offset < previous.offset) {
                    stop.offset = previous.offset;
                }
            }
            stops.add(stop);
            previous = stop;
        }
        return stops;
    }

    /**
     * Returns true if the specified list of URLs contains the specified url.
     *
     * @param urls the list of URLs
     * @param key the url to search for
     */
    private static boolean contains(List urls, ParsedURL key) {
        Iterator iter = urls.iterator();
        while (iter.hasNext()) {
            if (key.equals(iter.next()))
                return true;
        }
        return false;
    }

    /**
     * This class represents a gradient &lt;stop> element.
     */
    public static class Stop {

        /** The stop color. */
        public Color color;
        /** The stop offset. */
        public float offset;

        /**
         * Constructs a new stop definition.
         *
         * @param color the stop color
         * @param offset the stop offset
         */
        public Stop(Color color, float offset) {
            this.color = color;
            this.offset = offset;
        }
    }

    /**
     * Bridge class for the gradient &lt;stop> element.
     */
    public static class SVGStopElementBridge extends AbstractSVGBridge
        implements Bridge {

        /**
         * Returns 'stop'.
         */
        public String getLocalName() {
            return SVG_STOP_TAG;
        }

        /**
         * Creates a <tt>Stop</tt> according to the specified parameters.
         *
         * @param ctx the bridge context to use
         * @param gradientElement the gradient element
         * @param stopElement the stop element
         * @param opacity an additional opacity of the stop color
         */
        public Stop createStop(BridgeContext ctx,
                               Element gradientElement,
                               Element stopElement,
                               float opacity) {

            String s = stopElement.getAttributeNS(null, SVG_OFFSET_ATTRIBUTE);
            if (s.length() == 0) {
                throw new BridgeException(stopElement, ERR_ATTRIBUTE_MISSING,
                                          new Object[] {SVG_OFFSET_ATTRIBUTE});
            }
            float offset;
            try {
                offset = SVGUtilities.convertRatio(s);
            } catch (NumberFormatException ex) {
                throw new BridgeException
                    (stopElement, ERR_ATTRIBUTE_VALUE_MALFORMED,
                     new Object[] {SVG_OFFSET_ATTRIBUTE, s, ex});
            }
            Color color
                = CSSUtilities.convertStopColor(stopElement, opacity, ctx);

            return new Stop(color, offset);
        }
    }
}
