/*
 * The original file Graph.java (1.5 99/11/29) is
 * Copyright (c) 1997 Sun Microsystems, Inc. All Rights Reserved.
 * Adapted by F. Wienberg, 1999
 *
 * Sun grants you ("Licensee") a non-exclusive, royalty free, license to use,
 * modify and redistribute this software in source and binary code form,
 * provided that i) this copyright notice and license appear on all copies of
 * the software; and ii) Licensee does not utilize the software in a manner
 * which is disparaging to Sun.
 *
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
 * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR
 * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE
 * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING
 * OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS
 * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT,
 * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
 * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF
 * OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGES.
 *
 * This software is not designed or intended for use in on-line control of
 * aircraft, air traffic, aircraft navigation or aircraft communications; or in
 * the design, construction, operation or maintenance of any nuclear
 * facility. Licensee represents and warrants that it will not use or
 * redistribute the Software for such purposes.
 */
package de.renew.gui;

import java.awt.Dimension;
import java.awt.Point;
import java.util.Enumeration;
import java.util.Hashtable;

import CH.ifa.draw.framework.FigureChangeAdapter;
import de.renew.draw.storables.ontology.ConnectionFigure;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.FigureChangeEvent;

public class GraphLayout extends FigureChangeAdapter {
    public static final double DEFAULT_LENGTH_FACTOR = 1.0;
    public static final double DEFAULT_REPULSION_STRENGTH = 2.0;
    public static final double DEFAULT_REPULSION_LIMIT = 400.0;
    public static final double DEFAULT_SPRING_STRENGTH = 0.05;
    public static final double DEFAULT_TORQUE_STRENGTH = 0.25;
    public static final double DEFAULT_FRICTION_FACTOR = 0.75;
    public double lengthFactor = DEFAULT_LENGTH_FACTOR;
    public double repulsionStrength = DEFAULT_REPULSION_STRENGTH;
    public double repulsionLimit = DEFAULT_REPULSION_LIMIT;
    public double springStrength = DEFAULT_SPRING_STRENGTH;
    public double torqueStrength = DEFAULT_TORQUE_STRENGTH;
    public double frictionFactor = DEFAULT_FRICTION_FACTOR;
    final int repulsionType = 0; // 0: (1-r)/r   1: 1-r   2: (1-r)^2
    private Hashtable<Figure, GraphNode> nodes = new Hashtable<>();
    private Hashtable<ConnectionFigure, Double> edges = new Hashtable<>();

    public GraphLayout() {}

    private GraphNode getGraphNode(Figure node) {
        return nodes.get(node);
    }

    private double len(ConnectionFigure edge) {
        return edges.get(edge) * lengthFactor;
    }

    public void addNode(Figure node) {
        nodes.put(node, new GraphNode(node));
        node.addFigureChangeListener(this);
    }

    public void addEdge(ConnectionFigure edge, int addlen) {
        Dimension d1 = edge.start().owner().size();
        Dimension d2 = edge.end().owner().size();
        int len = Math.max(d1.width, d1.height) / 2 + Math.max(d2.width, d2.height) / 2 + addlen;
        edges.put(edge, (double) len);
    }

    public synchronized void relax() {
        if (nodes == null) {
            return;
        }
        Enumeration<ConnectionFigure> edgeEnum = edges.keys();
        while (edgeEnum.hasMoreElements()) {
            ConnectionFigure e = edgeEnum.nextElement();
            double targetlen = len(e);
            GraphNode from = getGraphNode(e.start().owner());
            GraphNode to = getGraphNode(e.end().owner());
            double vx = to.x - from.x;
            double vy = to.y - from.y;
            double len = Math.sqrt(vx * vx + vy * vy);

            if (len > 0) {
                double f = springStrength * (targetlen - len) / len;
                double dx = f * vx;
                double dy = f * vy;

                double phi = Math.atan2(vx, vy);
                double dir = -Math.sin(4 * phi);
                dx += torqueStrength * vy * dir / len;
                dy += -torqueStrength * vx * dir / len;

                to.dx += dx;
                to.dy += dy;
                from.dx += -dx;
                from.dy += -dy;
            }
        }

        Enumeration<GraphNode> nodeEnum1 = nodes.elements();
        while (nodeEnum1.hasMoreElements()) {
            GraphNode n1 = nodeEnum1.nextElement();
            double dx = 0;
            double dy = 0;

            Enumeration<GraphNode> nodeEnum2 = nodes.elements();
            while (nodeEnum2.hasMoreElements()) {
                GraphNode n2 = nodeEnum2.nextElement();
                if (n1 == n2) {
                    continue;
                }
                double vx = n1.x - n2.x;
                double vy = n1.y - n2.y;
                double lensqr = vx * vx + vy * vy;
                double len = Math.sqrt(lensqr);
                if (len == 0) {
                    dx += repulsionStrength * Math.random();
                    dy += repulsionStrength * Math.random();
                } else if (len < repulsionLimit) {
                    // Normalize length. 
                    vx = vx / repulsionLimit;
                    vy = vy / repulsionLimit;
                    len = len / repulsionLimit;
                    // Compute force.
                    double f = switch (repulsionType) {
                        case 0 -> 0.5 * (1 - len) / len;
                        case 1 -> 1 - len;
                        case 2 -> 2 * (1 - len) * (1 - len);
                        default -> 0;
                    };

                    f *= repulsionStrength;
                    dx += f * vx;
                    dy += f * vy;
                }
            }

            n1.dx += dx;
            n1.dy += dy;
        }

        Enumeration<Figure> nodeEnum = nodes.keys();
        while (nodeEnum.hasMoreElements()) {
            Figure node = nodeEnum.nextElement();
            GraphNode n = getGraphNode(node);
            if (!Boolean.TRUE.equals(node.getAttribute("Location"))) {
                n.x += Math.max(-5, Math.min(5, n.dx));
                n.y += Math.max(-5, Math.min(5, n.dy));
                Point c = node.center();
                node.moveBy((int) Math.round(n.x) - c.x, (int) Math.round(n.y) - c.y);

                if (n.x < 0) {
                    n.x = 0;
                }
                if (n.y < 0) {
                    n.y = 0;
                }
            }
            n.dx *= frictionFactor;
            n.dy *= frictionFactor;
        }
    }

    /**
     * Sent when a figure changed
     */
    @Override
    public synchronized void figureChanged(FigureChangeEvent e) {
        if (nodes != null) {
            Figure node = e.getFigure();
            if (nodes.containsKey(node)) {
                getGraphNode(node).update();
            }
        }
    }

    public void remove() {
        if (nodes != null) {
            Enumeration<Figure> nodeEnum = nodes.keys();
            while (nodeEnum.hasMoreElements()) {
                Figure node = nodeEnum.nextElement();
                node.removeFigureChangeListener(this);
            }
            nodes = null;
            edges = null;
        }
    }

    /**
     * Initialize positions with random values in the given rectangle.
     * Note that the figures are not actually moved: {@link #relax()}
     * has to be called for the figures to move.
     */
    synchronized void randomInit(double x, double y, double width, double height) {
        if (nodes != null) {
            Enumeration<GraphNode> nodeEnum = nodes.elements();
            while (nodeEnum.hasMoreElements()) {
                GraphNode node = nodeEnum.nextElement();

                node.x = x + Math.random() * width;
                node.y = y + Math.random() * height;
            }
        }
    }

    /**
     * Move all node figures uniformly.
     */
    synchronized void moveBy(int dx, int dy) {
        if (nodes != null) {
            Enumeration<Figure> figEnum = nodes.keys();
            while (figEnum.hasMoreElements()) {
                figEnum.nextElement().moveBy(dx, dy);
            }
        }
    }

    private static class GraphNode {
        double x = 0.0;
        double y = 0.0;
        double dx = 0.0;
        double dy = 0.0;
        final Figure node;

        GraphNode(Figure node) {
            this.node = node;
            update();
        }

        void update() {
            Point p = node.center();
            if (Math.abs(p.x - Math.round(x)) > 1 || Math.abs(p.y - Math.round(y)) > 1) {
                x = p.x;
                y = p.y;
            }
        }
    }
}