/*

   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.bridge;

import java.util.ArrayList;
import java.util.List;

import org.apache.batik.anim.AbstractAnimation;
import org.apache.batik.anim.AnimationEngine;
import org.apache.batik.anim.MotionAnimation;
import org.apache.batik.anim.dom.AnimationTarget;
import org.apache.batik.anim.dom.SVGOMElement;
import org.apache.batik.anim.dom.SVGOMPathElement;
import org.apache.batik.anim.values.AnimatableMotionPointValue;
import org.apache.batik.anim.values.AnimatableValue;
import org.apache.batik.ext.awt.geom.ExtendedGeneralPath;
import org.apache.batik.dom.svg.SVGAnimatedPathDataSupport;
import org.apache.batik.dom.util.XLinkSupport;
import org.apache.batik.parser.AWTPathProducer;
import org.apache.batik.parser.AngleHandler;
import org.apache.batik.parser.AngleParser;
import org.apache.batik.parser.LengthArrayProducer;
import org.apache.batik.parser.LengthPairListParser;
import org.apache.batik.parser.PathParser;
import org.apache.batik.parser.ParseException;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.svg.SVGAngle;

/**
 * Bridge class for the 'animateMotion' animation element.
 *
 * @author <a href="mailto:cam%40mcc%2eid%2eau">Cameron McCormack</a>
 * @version $Id: SVGAnimateMotionElementBridge.java 1802297 2017-07-18 13:58:12Z ssteiner $
 */
public class SVGAnimateMotionElementBridge extends SVGAnimateElementBridge {

    /**
     * Returns 'animateMotion'.
     */
    public String getLocalName() {
        return SVG_ANIMATE_MOTION_TAG;
    }

    /**
     * Returns a new instance of this bridge.
     */
    public Bridge getInstance() {
        return new SVGAnimateMotionElementBridge();
    }

    /**
     * Creates the animation object for the animation element.
     */
    protected AbstractAnimation createAnimation(AnimationTarget target) {
        animationType = AnimationEngine.ANIM_TYPE_OTHER;
        attributeLocalName = "motion";

        AnimatableValue from = parseLengthPair(SVG_FROM_ATTRIBUTE);
        AnimatableValue to = parseLengthPair(SVG_TO_ATTRIBUTE);
        AnimatableValue by = parseLengthPair(SVG_BY_ATTRIBUTE);

        boolean rotateAuto = false, rotateAutoReverse = false;
        float rotateAngle = 0;
        short rotateAngleUnit = SVGAngle.SVG_ANGLETYPE_UNKNOWN;
        String rotateString = element.getAttributeNS(null,
                                                     SVG_ROTATE_ATTRIBUTE);
        if (rotateString.length() != 0) {
            if (rotateString.equals("auto")) {
                rotateAuto = true;
            } else if (rotateString.equals("auto-reverse")) {
                rotateAuto = true;
                rotateAutoReverse = true;
            } else {
                class Handler implements AngleHandler {
                    float theAngle;
                    short theUnit = SVGAngle.SVG_ANGLETYPE_UNSPECIFIED;
                    public void startAngle() throws ParseException {
                    }
                    public void angleValue(float v) throws ParseException {
                        theAngle = v;
                    }
                    public void deg() throws ParseException {
                        theUnit = SVGAngle.SVG_ANGLETYPE_DEG;
                    }
                    public void grad() throws ParseException {
                        theUnit = SVGAngle.SVG_ANGLETYPE_GRAD;
                    }
                    public void rad() throws ParseException {
                        theUnit = SVGAngle.SVG_ANGLETYPE_RAD;
                    }
                    public void endAngle() throws ParseException {
                    }
                }
                AngleParser ap = new AngleParser();
                Handler h = new Handler();
                ap.setAngleHandler(h);
                try {
                    ap.parse(rotateString);
                } catch (ParseException pEx ) {
                    throw new BridgeException
                        (ctx, element,
                         pEx, ErrorConstants.ERR_ATTRIBUTE_VALUE_MALFORMED,
                         new Object[] { SVG_ROTATE_ATTRIBUTE, rotateString });
                }
                rotateAngle = h.theAngle;
                rotateAngleUnit = h.theUnit;
            }
        }
        return new MotionAnimation(timedElement,
                                   this,
                                   parseCalcMode(),
                                   parseKeyTimes(),
                                   parseKeySplines(),
                                   parseAdditive(),
                                   parseAccumulate(),
                                   parseValues(),
                                   from,
                                   to,
                                   by,
                                   parsePath(),
                                   parseKeyPoints(),
                                   rotateAuto,
                                   rotateAutoReverse,
                                   rotateAngle,
                                   rotateAngleUnit);
    }

    /**
     * Returns the parsed 'path' attribute (or the path from a referencing
     * 'mpath') from the animation element.
     */
    protected ExtendedGeneralPath parsePath() {
        Node n = element.getFirstChild();
        while (n != null) {
            if (n.getNodeType() == Node.ELEMENT_NODE
                    && SVG_NAMESPACE_URI.equals(n.getNamespaceURI())
                    && SVG_MPATH_TAG.equals(n.getLocalName())) {
                String uri = XLinkSupport.getXLinkHref((Element) n);
                Element path = ctx.getReferencedElement(element, uri);
                if (!SVG_NAMESPACE_URI.equals(path.getNamespaceURI())
                        || !SVG_PATH_TAG.equals(path.getLocalName())) {
                    throw new BridgeException
                        (ctx, element, ErrorConstants.ERR_URI_BAD_TARGET,
                         new Object[] { uri });
                }
                SVGOMPathElement pathElt = (SVGOMPathElement) path;
                AWTPathProducer app = new AWTPathProducer();
                SVGAnimatedPathDataSupport.handlePathSegList
                    (pathElt.getPathSegList(), app);
                return (ExtendedGeneralPath) app.getShape();
            }
            n = n.getNextSibling();
        }
        String pathString = element.getAttributeNS(null, SVG_PATH_ATTRIBUTE);
        if (pathString.length() == 0) {
            return null;
        }
        try {
            AWTPathProducer app = new AWTPathProducer();
            PathParser pp = new PathParser();
            pp.setPathHandler(app);
            pp.parse(pathString);
            return (ExtendedGeneralPath) app.getShape();
        } catch (ParseException pEx ) {
            throw new BridgeException
                (ctx, element, pEx, ErrorConstants.ERR_ATTRIBUTE_VALUE_MALFORMED,
                 new Object[] { SVG_PATH_ATTRIBUTE, pathString });
        }
    }

    /**
     * Returns the parsed 'keyPoints' attribute from the animation element.
     */
    protected float[] parseKeyPoints() {
        String keyPointsString =
            element.getAttributeNS(null, SVG_KEY_POINTS_ATTRIBUTE);
        int len = keyPointsString.length();
        if (len == 0) {
            return null;
        }
        List keyPoints = new ArrayList(7);
        int i = 0, start = 0, end;
        char c;
outer:  while (i < len) {
            while (keyPointsString.charAt(i) == ' ') {
                i++;
                if (i == len) {
                    break outer;
                }
            }
            start = i++;
            if (i != len) {
                c = keyPointsString.charAt(i);
                while (c != ' ' && c != ';' && c != ',') {
                    i++;
                    if (i == len) {
                        break;
                    }
                    c = keyPointsString.charAt(i);
                }
            }
            end = i++;
            try {
                float keyPointCoord =
                    Float.parseFloat(keyPointsString.substring(start, end));
                keyPoints.add(keyPointCoord);
            } catch (NumberFormatException nfEx ) {
                throw new BridgeException
                    (ctx, element, nfEx, ErrorConstants.ERR_ATTRIBUTE_VALUE_MALFORMED,
                     new Object[] { SVG_KEY_POINTS_ATTRIBUTE, keyPointsString });
            }
        }
        len = keyPoints.size();
        float[] ret = new float[len];
        for (int j = 0; j < len; j++) {
            ret[j] = (Float) keyPoints.get(j);
        }
        return ret;
    }

    /**
     * Returns the calcMode that the animation defaults to if none is specified.
     */
    protected int getDefaultCalcMode() {
        return MotionAnimation.CALC_MODE_PACED;
    }

    /**
     * Returns the parsed 'values' attribute from the animation element.
     */
    protected AnimatableValue[] parseValues() {
        String valuesString = element.getAttributeNS(null,
                                                     SVG_VALUES_ATTRIBUTE);
        int len = valuesString.length();
        if (len == 0) {
            return null;
        }
        return parseValues(valuesString);
    }

    protected AnimatableValue[] parseValues(String s) {
        try {
            LengthPairListParser lplp = new LengthPairListParser();
            LengthArrayProducer lap = new LengthArrayProducer();
            lplp.setLengthListHandler(lap);
            lplp.parse(s);
            short[] types = lap.getLengthTypeArray();
            float[] values = lap.getLengthValueArray();
            AnimatableValue[] ret = new AnimatableValue[types.length / 2];
            for (int i = 0; i < types.length; i += 2) {
                float x = animationTarget.svgToUserSpace
                    (values[i], types[i], AnimationTarget.PERCENTAGE_VIEWPORT_WIDTH);
                float y = animationTarget.svgToUserSpace
                    (values[i + 1], types[i + 1], AnimationTarget.PERCENTAGE_VIEWPORT_HEIGHT);
                ret[i / 2] = new AnimatableMotionPointValue(animationTarget, x, y, 0);
            }
            return ret;
        } catch (ParseException pEx ) {
            throw new BridgeException
                (ctx, element, pEx, ErrorConstants.ERR_ATTRIBUTE_VALUE_MALFORMED,
                 new Object[] { SVG_VALUES_ATTRIBUTE, s });
        }
    }

    /**
     * Parses a single comma-separated length pair.
     */
    protected AnimatableValue parseLengthPair(String ln) {
        String s = element.getAttributeNS(null, ln);
        if (s.length() == 0) {
            return null;
        }
        return parseValues(s)[0];
    }

    // AnimatableElement /////////////////////////////////////////////////////

    /**
     * Returns the underlying value of the animated attribute.  Used for
     * composition of additive animations.
     */
    public AnimatableValue getUnderlyingValue() {
        return new AnimatableMotionPointValue(animationTarget, 0f, 0f, 0f);
    }

    /**
     * Parses the animation element's target attributes and adds it to the
     * document's AnimationEngine.
     */
    protected void initializeAnimation() {
        // Determine the target element.
        String uri = XLinkSupport.getXLinkHref(element);
        Node t;
        if (uri.length() == 0) {
            t = element.getParentNode();
        } else {
            t = ctx.getReferencedElement(element, uri);
            if (t.getOwnerDocument() != element.getOwnerDocument()) {
                throw new BridgeException
                    (ctx, element, ErrorConstants.ERR_URI_BAD_TARGET,
                     new Object[] { uri });
            }
        }
        animationTarget = null;
        if (t instanceof SVGOMElement) {
            targetElement = (SVGOMElement) t;
            animationTarget = targetElement;
        }
        if (animationTarget == null) {
            throw new BridgeException
                (ctx, element, ErrorConstants.ERR_URI_BAD_TARGET,
                 new Object[] { uri });
        }

        // Add the animation.
        timedElement = createTimedElement();
        animation = createAnimation(animationTarget);
        eng.addAnimation(animationTarget, AnimationEngine.ANIM_TYPE_OTHER,
                         attributeNamespaceURI, attributeLocalName, animation);
    }
}
