/*
 * Copyright 2007 - 2016 the original author or authors.
 *
 * Licensed 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 net.sf.jailer.ui.graphical_view;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;

import javax.swing.ImageIcon;

import net.sf.jailer.datamodel.AggregationSchema;
import net.sf.jailer.datamodel.Association;
import net.sf.jailer.datamodel.Cardinality;
import prefuse.Constants;
import prefuse.render.EdgeRenderer;
import prefuse.util.GraphicsLib;
import prefuse.visual.EdgeItem;
import prefuse.visual.VisualItem;

/**
 * Renderer for {@link Association}s.
 * 
 * @author Ralf Wisser
 */
public class AssociationRenderer extends EdgeRenderer {

	// color setting
	public static Color COLOR_IGNORED;
	public static Color COLOR_ASSOCIATION;
	public static Color COLOR_DEPENDENCY;
	public static Color COLOR_REVERSE_DEPENDENCY;
	
	/**
	 * <code>true</code> for reversed rendering.
	 */
	boolean reversed;

	/**
	 * <code>true</code> for full rendering (for setting bounds).
	 */
	boolean full = false;

	/**
	 * Constructor.
	 * 
	 * @param reversed <code>true</code> for reversed rendering
	 */
	public AssociationRenderer(boolean reversed) {
		super(Constants.EDGE_TYPE_LINE, reversed? Constants.EDGE_ARROW_REVERSE : Constants.EDGE_ARROW_FORWARD);
		this.reversed = reversed;
	}

	/**
	 * Constructor.
	 */
	public AssociationRenderer() {
		full = true;
	}
    
	/**
	 * Temporary used in getRawShape.
	 */
	private Point2D m_isctPoints2[] = new Point2D[2];
	private Point2D starPosition = null;
	
	/**
     * Return a non-transformed shape for the visual representation of the
     * {@link Association}.
     * 
     * @param item the VisualItem being drawn
     * @return the "raw", untransformed shape
     */
	@Override
	protected Shape getRawShape(VisualItem item) {
		EdgeItem   edge = (EdgeItem)item;
		VisualItem item1 = edge.getSourceItem();
		VisualItem item2 = edge.getTargetItem();
		
		int type = m_edgeType;
		boolean reversedCurve = false;
		Association association = (Association) item.get("association");
		if (association != null && association.source == association.destination) {
			type = Constants.EDGE_TYPE_CURVE;
			reversedCurve = association.reversed;
		}
		
		getAlignedPoint(m_tmpPoints[0], item1.getBounds(),
		                m_xAlign1, m_yAlign1);
		getAlignedPoint(m_tmpPoints[1], item2.getBounds(),
		                m_xAlign2, m_yAlign2);
		m_curWidth = (float)(m_width * getLineWidth(item));
		EdgeItem e = (EdgeItem)item;
		
	    boolean forward = (m_edgeArrow == Constants.EDGE_ARROW_FORWARD);

	    // get starting and ending edge endpoints
	    Point2D start = null, end = null;
	    start = m_tmpPoints[forward?0:1];
	    end   = m_tmpPoints[forward?1:0];

	    if (!full) {
			double midX;
			double midY;
		    Point2D sp = start, ep = end;
		    
		    VisualItem dest = forward ? e.getTargetItem() : e.getSourceItem();
		    int i = GraphicsLib.intersectLineRectangle(start, end,
		            dest.getBounds(), m_isctPoints);
		    if ( i > 0 ) ep = m_isctPoints[0];

		    VisualItem src = !forward ? e.getTargetItem() : e.getSourceItem();
		    i = GraphicsLib.intersectLineRectangle(start, end,
		            src.getBounds(), m_isctPoints2);
		    if ( i > 0 ) sp = m_isctPoints2[0];
		    
	    	midX = (sp.getX() + ep.getX()) / 2;
			midY = (sp.getY() + ep.getY()) / 2;
			m_tmpPoints[reversed? 1 : 0].setLocation(midX, midY);
		}
		
		// create the arrow head, if needed
		if ( e.isDirected() && m_edgeArrow != Constants.EDGE_ARROW_NONE) {
		    if (type == Constants.EDGE_TYPE_CURVE) {
			    AffineTransform t = new AffineTransform();
		    	t.setToRotation(Math.PI/4 * (reversedCurve? 1 : -1));
		    	Point2D p = new Point2D.Double(), shift = new Point2D.Double();
		    	double d = start.distance(end) / 5.0;
		    	p.setLocation((end.getX() - start.getX()) / d, (end.getY() - start.getY()) / d);
		    	t.transform(p, shift);
		    	start.setLocation(start.getX() + shift.getX(), start.getY() + shift.getY());
		    	end.setLocation(end.getX() + shift.getX(), end.getY() + shift.getY());
		    }
	    	
		    // compute the intersection with the target bounding box
		    VisualItem dest = forward ? e.getTargetItem() : e.getSourceItem();
		    int i = GraphicsLib.intersectLineRectangle(start, end,
		            dest.getBounds(), m_isctPoints);
		    if ( i > 0 ) end = m_isctPoints[0];
		    
		    // create the arrow head shape
		    AffineTransform at = getArrowTrans(start, end, m_curWidth);
		    m_curArrow = at.createTransformedShape(m_arrowHead);
		    
		    // update the endpoints for the edge shape
		    // need to bias this by arrow head size
		    if (type == Constants.EDGE_TYPE_CURVE) {
		    	if (!"XML".equals(association.getDataModel().getExportModus()) || !isAggregation(association)) {
		    		m_curArrow = null;
		    	}
		    }
	    	Point2D lineEnd = m_tmpPoints[forward?1:0]; 
	    	lineEnd.setLocation(0, type == Constants.EDGE_TYPE_CURVE? 0 : -m_arrowHeight);
	    	at.transform(lineEnd, lineEnd);
		} else {
		    m_curArrow = null;
		}
		
		// create the edge shape
		Shape shape = null;
		double n1x = m_tmpPoints[0].getX();
		double n1y = m_tmpPoints[0].getY();
		double n2x = m_tmpPoints[1].getX();
		double n2y = m_tmpPoints[1].getY();
        m_line.setLine(n1x, n1y, n2x, n2y);
        shape = m_line;
        
        starBounds = null;
        starPosition = null;
        
        if (!forward && (Cardinality.MANY_TO_MANY.equals(association.getCardinality()) || Cardinality.MANY_TO_ONE.equals(association.getCardinality()))
        ||   forward && (Cardinality.MANY_TO_MANY.equals(association.getCardinality()) || Cardinality.ONE_TO_MANY.equals(association.getCardinality()))) {
        	starPosition = m_tmpPoints[forward? 1:0];
            start = starPosition;
        	end = m_tmpPoints[forward? 0:1];
		    AffineTransform t = new AffineTransform();
	    	t.setToRotation(-Math.PI/3);
	    	Point2D p = new Point2D.Double(), shift = new Point2D.Double();
	    	double d = m_tmpPoints[0].distance(m_tmpPoints[1]) / 9.0;
	    	p.setLocation((end.getX() - start.getX()) / d, (end.getY() - start.getY()) / d);
	    	t.transform(p, shift);
	    	starPosition.setLocation(starPosition.getX() + shift.getX(), starPosition.getY() + shift.getY());
	        starBounds = new Rectangle2D.Double(starPosition.getX() - STAR_SIZE * (starWidth / 2), starPosition.getY() - STAR_SIZE * (starHeight / 2), starWidth * STAR_SIZE, starHeight * STAR_SIZE);
        }

		return shape;
	}

	/**
     * Returns an affine transformation that maps the arrowhead shape
     * to the position and orientation specified by the provided
     * line segment end points.
     */
    protected AffineTransform getArrowTrans(Point2D p1, Point2D p2, 
                                            double width)
    {
        m_arrowTrans.setToTranslation(p2.getX(), p2.getY());
        m_arrowTrans.rotate(-HALF_PI + 
            Math.atan2(p2.getY()-p1.getY(), p2.getX()-p1.getX()));
        if ( width > 1 ) {
            double scalar = width/2;
            m_arrowTrans.scale(scalar, scalar);
        }
        return m_arrowTrans;
    }

    /**
     * Renders an {@link Association}.
     * 
     * @param g the 2D graphics
     * @param item visual item for the association
     * @param isSelected <code>true</code> for selected association
     */
    public void render(Graphics2D g, VisualItem item, boolean isSelected) {
		Association association = (Association) item.get("association");
		item.setSize(isSelected? 3 : 1);
		int color;
		if (!Boolean.TRUE.equals(item.get("full"))) {
			if (!full) {
				return;
			}
			color = associationColor(association);
		} else {
			if (full) {
				return;
			}
			color = reversed? associationColor(association.reversalAssociation) : associationColor(association);
		}
		item.setFillColor(color);
		item.setStrokeColor(color);
		BasicStroke stroke = item.getStroke();
		if (stroke != null) {
			if (reversed) {
				if (association != null) {
					association = association.reversalAssociation;
				}
			}
			if (association != null && association.isRestricted() && !association.isIgnored()) {
				item.setStroke(new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit(), 
					new float[] { 8f, 6f }, 1.0f));
			} else {
				item.setStroke(new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit()));
			}
		}
		if ("XML".equals(association.getDataModel().getExportModus())) {
			m_arrowHead = updateArrowHead(m_arrowWidth, m_arrowHeight, association, isSelected);
			arrowIsPotAggregation = true;
		} else {
			if (arrowIsPotAggregation) {
				m_arrowHead = updateArrowHead(m_arrowWidth, m_arrowHeight);
			}
			arrowIsPotAggregation = false;
		}
		starPosition = null;
		render(g, item);
		if (starPosition != null && starImage != null) {
			double size = STAR_SIZE;
		    transform.setTransform(size, 0, 0, size, starPosition.getX() - size * (starWidth / 2), starPosition.getY() - size * (starHeight / 2));
            g.drawImage(starImage, transform, null);
			starPosition = null;
		}
	}

    /**
     * @see prefuse.render.Renderer#setBounds(prefuse.visual.VisualItem)
     */
    public void setBounds(VisualItem item) {
    	super.setBounds(item);
        if (starBounds != null ) {
            Rectangle2D bbox = (Rectangle2D)item.get(VisualItem.BOUNDS);
            Rectangle2D.union(bbox, starBounds, bbox);
        }
    }

    private boolean arrowIsPotAggregation = false;
	private AffineTransform transform = new AffineTransform();
	private Rectangle2D starBounds = null;
	
    /**
     * Gets color for association.
     * 
     * @param association the association
     * @return the color for the association
     */
	private int associationColor(Association association) {
		if (association.isIgnored()) {
			return COLOR_IGNORED.getRGB();
		}
		if (association.isInsertDestinationBeforeSource()) {
			return COLOR_DEPENDENCY.getRGB();
		}
		if (association.isInsertSourceBeforeDestination()) {
			return COLOR_REVERSE_DEPENDENCY.getRGB();
		}
		return COLOR_ASSOCIATION.getRGB();
	}

	 /**
     * Returns true if the Point is located inside the extents of the item.
     * This calculation matches against the exact item shape, and so is more
     * sensitive than just checking within a bounding box.
     * 
     * @param p the point to test for containment
     * @param item the item to test containment against
     * @return true if the point is contained within the the item, else false
     */
	@Override
	public boolean locatePoint(Point2D p, VisualItem item) {
		Shape s = getShape(item);
		if ( s == null ) {
		    return false;
		} else {
		    double width = Math.max(14, getLineWidth(item));
		    double halfWidth = width/2.0;
		    return s.intersects(p.getX()-halfWidth,
		                        p.getY()-halfWidth,
		                        width,width);
		}
	}

	/**
	 * Render aggregation symbols.
	 */
	protected Polygon updateArrowHead(int w, int h, Association association, boolean isSelected) {
		if (isAggregation(association)) {
			if ( m_arrowHead == null ) {
			    m_arrowHead = new Polygon();
			} else {
			    m_arrowHead.reset();
			}
			double ws = 0.9;
			double hs = 2.0/3.0;
			if (isSelected) {
				ws /= 1.3;
				hs /= 1.3;
			}
			m_arrowHead.addPoint(0, 0);
			m_arrowHead.addPoint((int) (ws*-w), (int) (hs*(-h)));
			m_arrowHead.addPoint( 0, (int) (hs*(-2*h)));
			m_arrowHead.addPoint((int) (ws*w), (int) (hs*(-h)));
			m_arrowHead.addPoint(0, 0);
			return m_arrowHead;
		} else {
			return updateArrowHead(w, h);
		}
	}

	/**
	 * Checks whether association must be rendered as aggregation.
	 * 
	 * @param association the association to check
	 * @return <code>true</code> if association must be rendered as aggregation
	 */
	private boolean isAggregation(Association association) {
		return association.reversalAssociation.getAggregationSchema() != AggregationSchema.NONE;
	}
	
	private Image starImage = null;
	private double starWidth = 0;
	private double starHeight = 0;
	private final double STAR_SIZE = 0.22;
	{
		// load images
		try {
			String dir = "/net/sf/jailer/resource";
			
			starImage = new ImageIcon(getClass().getResource(dir + "/star.png")).getImage();
			starWidth = starImage.getWidth(null);
			starHeight = starImage.getHeight(null);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
}
