package ca.ualberta.stothard.cgview;

import java.awt.*;
import java.awt.font.TextLayout;
import java.awt.font.FontRenderContext;
import java.awt.geom.*;
import java.util.*;
import java.util.regex.*;

/**
 * This class is used by Cgview objects to facilitate label layout and drawing.
 *
 * @author Paul Stothard
 */

public abstract class Label implements CgviewConstants {

    protected String labelText;
    protected Color color;
    protected int strand;

    protected String hyperlink;
    protected String mouseover;

    protected double lineStartRadians;
    protected double lineStartRadius;
    protected double lineEndRadians;
    protected double lineEndRadius;

    protected boolean extendedRadius;
    protected double extendedLineStartRadius;
    protected double extendedLineEndRadius;

    protected boolean fixedInPlace;
    protected double allowedRadiansDelta;

    protected static double radiusShiftAmount;
    protected static double radiansShiftAmount;

    protected Rectangle2D unplacedBounds;
    protected Rectangle2D placedBounds;

    protected Font font;
    protected float descent;
    protected float ascent;

    protected boolean forceLabel;

    protected Cgview cgview;

    protected static double smallestDimension = 0;

    protected static double RADIAN_SHIFT_PADDING = 3.0d;

    /**
     * Constructs a new Label object.
     *
     * @param cgview           the Cgview object to contain this Label.
     * @param labelText        the text that is to be drawn.
     * @param hyperlink        a hyperlink to be associated with this Label.
     * @param mouseover        mouseover information to be associated with this Label.
     * @param font             the font to use when drawing this Label.
     * @param color            the color to use when drawing this Label.
     * @param forceLabel       whether or not this Label should be drawn even if it cannot be placed such that it does
     *                         not clash with other labels.
     * @param lineStartRadians the angle in radians of the line extending from the feature to this Label.
     * @param strand           the strand of this Label ({@link CgviewConstants#DIRECT_STRAND} or {@link
     *                         CgviewConstants#REVERSE_STRAND}).
     */
    protected Label(Cgview cgview, String labelText, String hyperlink, String mouseover, Font font, Color color, boolean forceLabel, double lineStartRadians, int strand) {
        this.cgview = cgview;
        this.labelText = labelText;
        this.hyperlink = hyperlink;
        this.mouseover = mouseover;
        this.color = color;
        this.forceLabel = forceLabel;
        this.lineStartRadians = lineStartRadians;
        this.strand = strand;

        if (this.labelText == null) {
            this.labelText = "Untitled";
        }

        Pattern p = Pattern.compile("\\S");
        Matcher m = p.matcher(this.labelText);

        if (!(m.find())) {
            this.labelText = "Untitled";
        }

        if (font != null) {
            this.font = font;
        } else {
            this.font = cgview.getLabelFont();
        }

        lineEndRadians = lineStartRadians;

        //if close to vertical
        if (Math.abs(Math.sin(lineStartRadians)) > 0.70d) {
            allowedRadiansDelta = (1.0d / 16.0d) * (2.0d * Math.PI);
        } else {
            allowedRadiansDelta = (1.0d / 10.0d) * (2.0d * Math.PI);
        }
        extendedRadius = false;
        fixedInPlace = false;

        //create bounds
        Graphics2D gg = cgview.getGraphics();
        FontRenderContext frc = gg.getFontRenderContext();
        TextLayout layout = new TextLayout(this.labelText, this.font, frc);

        unplacedBounds = layout.getBounds();
        placedBounds = layout.getBounds();
        descent = layout.getDescent();
        ascent = layout.getAscent();

        if (smallestDimension == 0) {
            if (unplacedBounds.getWidth() > unplacedBounds.getHeight()) {
                smallestDimension = unplacedBounds.getHeight();
            } else {
                smallestDimension = unplacedBounds.getWidth();
            }
        } else {
            if (unplacedBounds.getWidth() < smallestDimension) {
                smallestDimension = unplacedBounds.getWidth();
            }
            if (unplacedBounds.getHeight() < smallestDimension) {
                smallestDimension = unplacedBounds.getHeight();
            }
        }


        radiusShiftAmount = cgview.getRadiusShiftAmount();

        //radiansShiftConstant may need to be made smaller if label lines are crossing
        //radiansShiftAmount = cgview.getRadiansShiftConstant() / cgview.getLastOuterFeatureRadius();
        radiansShiftAmount = ((smallestDimension / 2) / (cgview.getLastOuterFeatureRadius()));


        //need to adjust some values for zooming
        //radiansShiftAmount = radiansShiftAmount / cgview.getZoomMultiplier();
        //allowedRadiansDelta = allowedRadiansDelta / cgview.getZoomMultiplier();
    }

    protected abstract void drawLabelText();

    protected abstract void drawLabelLine();

    /**
     * Draws a line between two points described by radius and radian values.
     *
     * @param startRadius  the radius of the first point.
     * @param startRadians the radians of the first point.
     * @param endRadius    the radius of the second point.
     * @param endRadians   the radians of the second point.
     */
    protected void drawLine(double startRadius, double startRadians, double endRadius, double endRadians) {

        Graphics2D gg = cgview.getGraphics();

        float labelLineThickness = cgview.getLabelLineThickness();

        double lineX1 = Math.cos(startRadians) * startRadius;
        double lineY1 = Math.sin(startRadians) * startRadius;
        double lineX2 = Math.cos(endRadians) * endRadius;
        double lineY2 = Math.sin(endRadians) * endRadius;

        if (cgview.getGlobalLabelColor() != null) {
            gg.setPaint(cgview.getGlobalLabelColor());
        } else {
            gg.setPaint(color);
        }

        gg.setStroke(new BasicStroke(labelLineThickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER));
        gg.draw(new Line2D.Double(lineX1, lineY1, lineX2, lineY2));

    }

    /**
     * Draws a line between two points described by radius and radian values.
     *
     * @param startRadius  the radius of the first point.
     * @param startRadians the radians of the first point.
     * @param endRadius    the radius of the second point.
     * @param endRadians   the radians of the second point.
     * @param area         an area to add to the line.
     */
    protected void drawLine(double startRadius, double startRadians, double endRadius, double endRadians, Area area) {

        Graphics2D gg = cgview.getGraphics();

        float labelLineThickness = cgview.getLabelLineThickness();

        double lineX1 = Math.cos(startRadians) * startRadius;
        double lineY1 = Math.sin(startRadians) * startRadius;
        double lineX2 = Math.cos(endRadians) * endRadius;
        double lineY2 = Math.sin(endRadians) * endRadius;

        if (cgview.getGlobalLabelColor() != null) {
            gg.setPaint(cgview.getGlobalLabelColor());
        } else {
            gg.setPaint(color);
        }

	BasicStroke lineStroke = new BasicStroke(labelLineThickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER);
	area.add(new Area(lineStroke.createStrokedShape(new Line2D.Double(lineX1, lineY1, lineX2, lineY2))));
	
        gg.fill(area);

    }

    /**
     * Returns an Area representing a line between two points described by radius and radian values.
     *
     * @param startRadius  the radius of the first point.
     * @param startRadians the radians of the first point.
     * @param endRadius    the radius of the second point.
     * @param endRadians   the radians of the second point.
     * @return an Area to add to the line.
     */
    protected Area getLineAsArea(double startRadius, double startRadians, double endRadius, double endRadians) {

        Graphics2D gg = cgview.getGraphics();

        float labelLineThickness = cgview.getLabelLineThickness();

        double lineX1 = Math.cos(startRadians) * startRadius;
        double lineY1 = Math.sin(startRadians) * startRadius;
        double lineX2 = Math.cos(endRadians) * endRadius;
        double lineY2 = Math.sin(endRadians) * endRadius;

	Area area = new Area();

	BasicStroke lineStroke = new BasicStroke(labelLineThickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER);
	area.add(new Area(lineStroke.createStrokedShape(new Line2D.Double(lineX1, lineY1, lineX2, lineY2))));
	
        return area;

    }

    /**
     * Returns the starting point of the line drawn to this Label.
     *
     * @return the starting point of the line drawn to this Label.
     */
    protected Point2D getLineStart() {
        return new Point2D.Double(Math.cos(this.lineStartRadians) * this.lineStartRadius, Math.sin(this.lineStartRadians) * this.lineStartRadius);
    }

    /**
     * Returns the radians of the innermost point in the line extending from the feature to this Label.
     *
     * @return the radians of the innermost point in the label line.
     */
    protected double getLineStartRadians() {
        return lineStartRadians;
    }

    /**
     * Returns the radians of the outermost point in the line extending from the feature to this Label.
     *
     * @return the radians of the outermost point in the label line.
     */
    protected double getLineEndRadians() {
        return lineEndRadians;
    }

    /**
     * Returns a boolean specifying whether or not the line between the feature and this Label has been extended to
     * prevent label clashes.
     *
     * @return a <code>boolean</code> representing whether or not the line between the feature and this Label has been
     *         extended to prevent label clashes.
     */
    protected boolean isExtendedRadius() {
        return extendedRadius;
    }

    /**
     * Returns the radius of the innermost point in the line extending from the feature to this Label.
     *
     * @return the radius of the innermost point in the label line.
     */
    protected double getLineStartRadius() {
        return lineStartRadius;
    }

    /**
     * Returns the radius of the outermost point in the line extending from the feature to this Label.
     *
     * @return the radius of the outermost point in the label line.
     */
    protected double getLineEndRadius() {
        return lineEndRadius;
    }

    /**
     * Returns the radius of the outermost point in the second line (the extended line) extending from the feature to
     * this Label.
     *
     * @return the radius of the outermost point in the second label line.
     */
    protected double getExtendedLineEndRadius() {
        return extendedLineEndRadius;
    }

    /**
     * Returns a boolean specifying whether or not this label should be drawn even if it cannot be placed such that it
     * does not clash with other labels.
     *
     * @return a <code>boolean</code> specifying whether or not this Label should be drawn even if it cannot be placed
     *         such that it does not clash with other labels.
     */
    protected boolean getForceLabel() {
        return forceLabel;
    }

    protected abstract void updateBounds();

    protected abstract void updateBounds(double padding);

    /**
     * Returns a rectangle that represents the bounds of this Label. {@link #updateBounds()} should be used to calculate
     * the bounds if the Label has been moved since the last call to {@link #updateBounds()}.
     *
     * @return the bounds of this Label.
     */
    protected Rectangle2D getBounds() {
        return placedBounds;
    }

    /**
     * Returns a boolean specifying whether or not this Label clashes with the supplied Label.
     *
     * @param testLabel a Label object.
     * @return a <code>boolean</code> specifying whether this label clashes with the supplied label.
     */
    protected boolean clashes(Label testLabel) {
        return this.getBounds().intersects(testLabel.getBounds());
    }

    protected abstract boolean clashesWithAny();

    /**
     * Attempts to move this Label by decreasing its radians value. If the angle between the the start of the label line
     * and the label text is at the maximum acceptable value the label is not moved.
     *
     * @return a <code>boolean</code> specifing whether or not this Label was moved.
     */
    protected final boolean shiftRadiansLower() {
        if (Math.abs(lineStartRadians - (lineEndRadians - radiansShiftAmount)) < (allowedRadiansDelta)) {
            lineEndRadians = lineEndRadians - radiansShiftAmount;
            updateBounds(3.0d);
            return true;
        } else {
            //System.out.println("failed to shift radians lower");
            return false;
        }
    }

    /**
     * Attempts to move this Label by increasing its radians value. If the angle between the the start of the label line
     * and the label text is at the maximum acceptable value the label is not moved.
     *
     * @return a <code>boolean</code> specifing whether or not this Label was moved.
     */
    protected final boolean shiftRadiansHigher() {
        if (Math.abs(lineStartRadians - (lineEndRadians + radiansShiftAmount)) < (allowedRadiansDelta)) {
            lineEndRadians = lineEndRadians + radiansShiftAmount;
            updateBounds(3.0d);
            return true;
        } else {
            //System.out.println("failed to shift radians higher");
            return false;
        }

    }

    /**
     * Attempts to move this Label by adjusting its radians value such that it is closer to its original radians value.
     * If the adjustment introduces a conflict for space with other labels, this Label is not moved.
     *
     * @return a <code>boolean</code> specifing whether or not this Label was moved.
     */
    protected boolean shiftRadiansToOriginal(ArrayList labels) {

        if (lineEndRadians == lineStartRadians) {
            //they are already the same
            return false;
        } else if (Math.abs(lineStartRadians - lineEndRadians) <= (radiansShiftAmount)) {
            //already close enough, make them the same if possible
            double tempRadians = lineEndRadians;
            lineEndRadians = lineStartRadians;
            updateBounds(3.0d);
            if (clashesWithAny(labels)) {
                lineEndRadians = tempRadians;
                updateBounds(3.0d);
                return false;
            } else {
                return false;
            }
        } else if (lineStartRadians < lineEndRadians) {
            shiftRadiansLower();
            if (clashesWithAny(labels)) {
                shiftRadiansHigher();
                return false;
            } else {
                return true;
            }
        } else if (lineStartRadians > lineEndRadians) {
            this.shiftRadiansHigher();
            if (clashesWithAny(labels)) {
                shiftRadiansLower();
                return false;
            } else {
                return true;
            }
        }
        return false;
    }

    protected abstract boolean extendRadius();

    protected abstract void setLineStartRadius(double lineStartRadius);

    /**
     * Returns a boolean specifying whether or not this Label clashes with any of the labels in the supplied ArrayList.
     *
     * @param labels a list of Label objects.
     * @return a <code>boolean</code> specifying whether or not this Label clashes with any of the Label objects in the
     *         supplied list.
     */
    protected boolean clashesWithAny(ArrayList labels) {

        if (labels.size() < 2) {
            return false;
        }
        for (int outer = 0; outer < labels.size(); outer++) {
            Label outerLabel = (Label) (labels.get(outer));
            if (labels.indexOf(this) == outer) {
                continue;
            } else if (this.clashes(outerLabel)) {
                return true;
            }

        }
        return false;
    }

    protected abstract boolean fitsInBackground();

    /**
     * Specifies the hyperlink to be associated with this Label.
     *
     * @param hyperlink the hyperlink to be associated with this Label.
     */
    protected void setHyperlink(String hyperlink) {
        this.hyperlink = hyperlink;
    }

    /**
     * Returns the hyperlink to be associated with this Label.
     *
     * @return the hyperlink to be associated with this Label.
     */
    protected String getHyperlink() {
        return hyperlink;
    }

    /**
     * Returns the text of this Label.
     *
     * @return the text of this Label.
     */
    protected String getLabelText() {
        return labelText;
    }

    /**
     * The mouseover to be associated with this Label.
     *
     * @param mouseover the mouseover to be associated with this Label.
     */
    protected void setMouseover(String mouseover) {
        this.mouseover = mouseover;
    }

    /**
     * Returns the mouseover to be associated with this Label.
     *
     * @return the mouseover to be associated with this Label.
     */
    protected String getMouseover() {
        return mouseover;
    }

}
