package at.oefai.aaa.animation;

import java.util.Date;
import java.util.Random;

import org.apache.batik.dom.svg.SVGDOMImplementation;
import org.apache.batik.util.SVGConstants;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.svg.SVGCircleElement;
import org.w3c.dom.svg.SVGElement;
import org.w3c.dom.svg.SVGGElement;
import org.w3c.dom.svg.SVGMatrix;
import org.w3c.dom.svg.SVGPoint;
import org.w3c.dom.svg.SVGSVGElement;
import org.w3c.dom.svg.SVGSymbolElement;
import org.w3c.dom.svg.SVGTextElement;
import org.w3c.dom.svg.SVGUseElement;

/**
 * baseAnim for all animations that deal with SVG.
 * @author Stefan Rank
 */
abstract class BaseSVGAnim extends AbstractBaseAnim {
    // contains commonly used svg manipulation routines
    private static final Random RAND = new Random(new Date().getTime());
    private static final float AGENT_STEPLENGTH = 7; // units per step
    private static final float AGENT_STEPLENGTH_SQUARED = AGENT_STEPLENGTH * AGENT_STEPLENGTH;
    private static final float OBJECT_STEPLENGTH = 4; // units per step
    private static final float OBJECT_STEPLENGTH_SQUARED = OBJECT_STEPLENGTH * OBJECT_STEPLENGTH;
    private static final float BASE_FONT_SIZE = 11f;
    protected static final String SHADOW_POSTFIX = "Shadow";
    private static final String HOLDING_POSTFIX = "Holding";
    private static final String NONHOLDING_POSTFIX = "NonHolding";
    private static final String TEXT_POSTFIX = "Text";

    // no comment
    protected static final float square(final float x) {
        return x * x;
    }

    /**
     * sets x and y of eleAbove, so that it can be added as child of eleBelow.
     * @param eleAbove
     * @param eleBelow
     */
    protected SVGPoint getCoordsOfOntoObject(final SVGSVGElement eleAbove, final SVGSVGElement eleBelow) {
        SVGPoint p = eleBelow.createSVGPoint();
        // set its x to 0 (directly above)
        p.setX(0);
        // and y to minus the sum of the heights (height is half the height of the object)
        // (and use only half the height of the below object to get overlap)
        p.setY(-(eleAbove.getHeight().getBaseVal().getValue()
                 + 0.5f * eleBelow.getHeight().getBaseVal().getValue()));
        return p;
    }

    protected SVGPoint getCoordsOfAgentsObject(final SVGSVGElement eleAbove, final SVGSVGElement eleAgentBelow) {
        SVGPoint p = eleAgentBelow.createSVGPoint();
        // and x to 0 +- a random between 0 and agentwidth
        float agentwidthhalf = 0.5f * eleAgentBelow.getWidth().getBaseVal().getValue();
        p.setX(RAND.nextFloat() * agentwidthhalf - agentwidthhalf);
        // set its y to 0 (on the agent)
        float agentheight = eleAgentBelow.getHeight().getBaseVal().getValue();
        p.setY(-0.5f * agentheight);
        return p;
    }

    protected void addToSVGPoint(final SVGPoint resultant, final SVGPoint toAdd) {
        resultant.setX(resultant.getX() + toAdd.getX());
        resultant.setY(resultant.getY() + toAdd.getY());
    }

    /**
     * returns a point in the circle of the position (circle element) with id name.
     * @param name
     * @return randomly placed point in the circle
     */
    protected SVGPoint getNamedPointRandDisplace(final String name) {
        SVGPoint point = this.svgDoc.getRootElement().createSVGPoint();
        SVGCircleElement elePos = (SVGCircleElement) this.svgDoc.getElementById(name);
        if (elePos == null) {
            assert false : "could not get position circle (" + name + ")";
            return null;
        }
        // now calculate destinationCoords
        point.setX(elePos.getCx().getBaseVal().getValue());
        point.setY(elePos.getCy().getBaseVal().getValue());
        float destR = elePos.getR().getBaseVal().getValue();
        float doubler = 2f * destR;
        point.setX(point.getX() + (RAND.nextFloat() * doubler - destR));
        point.setY(point.getY() + (RAND.nextFloat() * doubler - destR));
        return point;
    }

    /**
     * displace a point by a named circle elements x,y point.
     * @param p the point to displace ATTENTION works by side effect
     * @param name the named circles name
     */
    protected void displacePointByNamed(final SVGPoint p, final String name) {
        SVGCircleElement eleDisplace = (SVGCircleElement) this.svgDoc.getElementById(name);
        if (eleDisplace != null) {
            p.setX(p.getX() + eleDisplace.getCx().getBaseVal().getValue());
            p.setY(p.getY() + eleDisplace.getCy().getBaseVal().getValue());
        }
    }


    /**
     * returns the element with id name.
     * @param name
     * @return the unique element by id
     */
    protected SVGSVGElement getNamedElement(final String name) {
        SVGSVGElement ele = (SVGSVGElement) this.svgDoc.getElementById(name);
        if (ele == null) {
            assert false : "could not get svgelement (" + name + ")";
            return null;
        }
        return ele;
    }


    /**
     * returns the current pos of the element.
     * @param ele
     * @return position in parent SVG
     */
    protected SVGPoint getElementsPos(final SVGSVGElement ele) {
        if (ele == null) {
            return null;
        }
        SVGPoint point = ele.createSVGPoint();
        point.setX(ele.getX().getBaseVal().getValue());
        point.setY(ele.getY().getBaseVal().getValue());
        return point;
    }

    protected SVGPoint getElementsDocumentPos(final SVGSVGElement ele) {
        if (ele == null) {
            return null;
        }
        SVGPoint point = ele.createSVGPoint();
        SVGMatrix mat = ele.getTransformToElement(this.svgDoc.getRootElement());
        point.setX(mat.getE() / mat.getA());
        point.setY(mat.getF() / mat.getD());
        return point;
    }


    /**
     * sets x and y of the element.
     * @param ele
     * @param pos
     */
    protected void setElementsPos(final SVGSVGElement ele, final SVGPoint pos) {
        if (ele == null || pos == null) {
            return;
        }
        ele.getX().getBaseVal().setValue(pos.getX());
        ele.getY().getBaseVal().setValue(pos.getY());
    }

    /**
     * returns a point agentStepLength in the direction towards dest or dest itself.
     * @param curr
     * @param dest
     * @return next position to move to
     */
    protected SVGPoint getNextAgentPos(final SVGPoint curr, final SVGPoint dest) {
        return getNextPos(curr, dest, AGENT_STEPLENGTH, AGENT_STEPLENGTH_SQUARED);
    }

    /**
     * returns a point objectStepLength in the direction towards dest or dest itself.
     * @param curr
     * @param dest
     * @return next position to move the object to
     */
    protected SVGPoint getNextObjectPos(final SVGPoint curr, final SVGPoint dest) {
        return getNextPos(curr, dest, OBJECT_STEPLENGTH, OBJECT_STEPLENGTH_SQUARED);
    }

    /**
     * returns a point moveStepLength in the direction towards dest or dest itself.
     * @param curr
     * @param dest
     * @return
     */
    private SVGPoint getNextPos(final SVGPoint curr, final SVGPoint dest, final float stepLength, final float stepLengthSquared) {
        assert (dest != null && curr != null) : "needs non null arguments";
        float distX = dest.getX() - curr.getX();
        float distY = dest.getY() - curr.getY();
        float distance = square(distX) + square(distY);
        if (distance <= stepLengthSquared) { // near enough
            return dest;
        }
        SVGPoint point = this.svgDoc.getRootElement().createSVGPoint();
        float ratio = stepLength / (float) Math.sqrt(distance);
        point.setX(curr.getX() + distX * ratio);
        point.setY(curr.getY() + distY * ratio);
        return point;
    }


    /**
     * set a rotation on an element or an appropriately named subelement if present.
     * @param ele
     * @param rotation
     */
    protected void setElementsRotate(final SVGSVGElement ele, final float rotation) {
        SVGElement eleRotate = (SVGElement) ele.getElementById(ele.getId() + "Rotate");
        if (eleRotate == null) { // fallback if no special rotate element
            eleRotate = ele;
        }
        if (Math.abs(rotation) < Double.MIN_VALUE * 2) { // accomodate rounding errors
            eleRotate.setAttributeNS(null, "transform", "");
        } else {
            eleRotate.setAttributeNS(null, "transform", "rotate(" + rotation + ")");
        }
    }

    /**
     * appends an SVGSVGElement representing an object at the suitable location.
     * @param e
     */
    protected void appendObjectToSVGDoc(final SVGSVGElement e) {
        // if there is a designated Objects group in the svg use it
        Element a = this.svgDoc.getElementById("Objects");
        if (a == null || !(a instanceof SVGGElement)) {
            this.svgDoc.getRootElement().appendChild(e);
        } else {
            a.appendChild(e);
        }
    }


    protected Element showElement(final String name) {
        if (name == null) {
            return null;
        }
        Element ele = this.svgDoc.getElementById(name);
        if (ele != null) {
            ele.setAttributeNS(null, "visibility", "visible");
        }
        return ele;
    }

    protected void hideElement(final String name) {
        if (name == null) {
            return;
        }
        Element ele = this.svgDoc.getElementById(name);
        if (ele != null) {
            ele.setAttributeNS(null, "visibility", "hidden");
        }
    }


    protected Element showTextElement(final String pActor, final String name, final float intensity) {
        if (pActor == null || name == null) {
            return null;
        }
        Element ele = showElement(this.actor + name + TEXT_POSTFIX);
        if (ele == null) { // create a new one
            return createReplacementText(pActor, name, intensity);
        }
        if (intensity != 0f) { // just set the intensity value
            updateReplacementText(ele, name, intensity);
        }
        return ele;
    }

    protected void hideTextElement(final String pActor, final String name) {
        hideElement(pActor + name + TEXT_POSTFIX);
    }


    protected void adjustHolding(final SVGSVGElement ele) {
        NodeList nl = ele.getElementsByTagNameNS(SVGConstants.SVG_NAMESPACE_URI, "svg");
        if (nl.getLength() > 0) {
            hideElement(ele.getId() + NONHOLDING_POSTFIX);
            showElement(ele.getId() + HOLDING_POSTFIX);
        } else {
            hideElement(ele.getId() + HOLDING_POSTFIX);
            showElement(ele.getId() + NONHOLDING_POSTFIX);
        }
    }


    private Element createReplacementText(final String pActor, final String text, final float intense) {
        if (pActor == null || text == null) {
            return null;
        }
        SVGSVGElement eleActor = getNamedElement(pActor);
        if (eleActor == null) {
            return null;
        }
        SVGGElement gEle = (SVGGElement) this.svgDoc.createElementNS(SVGConstants.SVG_NAMESPACE_URI, "g");
        gEle.setId(pActor + text + TEXT_POSTFIX);
        // now correct displacement
        float actorHeight = eleActor.getHeight().getBaseVal().getValue();
        gEle.setAttributeNS(null, "transform", "translate(0 -" + (2f * actorHeight) +  ")");
        //if there is a balloon symbol use it:
        Element symbolEle = this.svgDoc.getElementById("BalloonSymbol");
        if (symbolEle != null && symbolEle instanceof SVGSymbolElement) {
            SVGUseElement useEle = (SVGUseElement) this.svgDoc.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, "use");
            useEle.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#BalloonSymbol");
            gEle.appendChild(useEle);
        }
        SVGTextElement textEle = (SVGTextElement) this.svgDoc.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, "text");
        textEle.appendChild(this.svgDoc.createTextNode(text));
        textEle.setAttributeNS(null, "text-anchor", "middle");
        textEle.setAttributeNS(null, "font-size", Integer.toString(Math.round(BASE_FONT_SIZE * intense)));
        gEle.appendChild(textEle);
        eleActor.appendChild(gEle);
        return gEle;
    }

    private Element updateReplacementText(final Element ele, final String text, final float intense) {
        if (ele == null || text == null) {
            return null;
        }
        NodeList nl = ele.getElementsByTagNameNS(SVGDOMImplementation.SVG_NAMESPACE_URI, "text");
        if (nl.getLength() > 0) {
            SVGTextElement textEle = (SVGTextElement) nl.item(0);
            //textEle.replaceChild(svgDoc.createTextNode(text + intense), textEle.getFirstChild());
            textEle.setAttributeNS(null, "font-size", Integer.toString(Math.round(BASE_FONT_SIZE * intense)));
        }
        return ele;
    }


}
