package ca.ualberta.stothard.cgview;

import java.awt.*;
import java.util.*;

/**
 * This class represents a sequence feature.  Feature objects (along with the FeatureRange objects) are used to describe
 * individual sequence features. Feature objects can specify, for example, the color, opacity, and thickness of a
 * sequence feature. They can also be used to provide a text label. The Feature object does not dictate which bases
 * contain the sequence feature. The position information is instead provided by FeatureRange objects contained by the
 * Feature objects.
 *
 * @author Paul Stothard
 */

public class Feature implements CgviewConstants {

    private String label;
    private String hyperlink;
    private String mouseover;
    private ArrayList ranges = new ArrayList();
    private Color color = new Color(0, 0, 255); //blue
    private int strand;
    private Font font;
    private boolean forceLabel = false;
    private int showLabel = LABEL;

    private int decoration = DECORATION_STANDARD;
    private float radiusAdjustment = 0.0f;
    private float proportionOfThickness = 1.0f;
    private float opacity = 1.0f;

    private boolean showShading;

    /**
     * Constructs a new Feature object.
     *
     * @param featureSlot the FeatureSlot object to contain this Feature.
     * @param label       the text label for this Feature. This label will be used in any labels generated for the
     *                    Feature when it is drawn.
     */
    public Feature(FeatureSlot featureSlot, String label) {
        this.label = label;
        this.strand = featureSlot.getStrand();
        this.showShading = featureSlot.getShowShading();
        //add this feature to the FeatureSlot.
        featureSlot.addFeature(this);
    }

    /**
     * Constructs a new Feature object.
     *
     * @param featureSlot the FeatureSlot object to contain this Feature.
     */
    public Feature(FeatureSlot featureSlot) {
        this.strand = featureSlot.getStrand();
        this.showShading = featureSlot.getShowShading();
        //add this feature to the FeatureSlot.
        featureSlot.addFeature(this);
    }

    /**
     * Constructs a new Feature object. The Feature object must be added to a FeatureSlot using the {@link
     * #setFeatureSlot(FeatureSlot) setFeatureSlot()} method. method.
     */
    public Feature() {

    }

    /**
     * Constructs a new Feature object. The Feature object must be added to a FeatureSlot using the {@link
     * #setFeatureSlot(FeatureSlot) setFeatureSlot()} method. method.
     *
     * @param showShading whether or not this Feature should be drawn with shading.
     */
    public Feature(boolean showShading) {
        this.showShading = showShading;
    }

    /**
     * Places this Feature into a FeatureSlot.
     *
     * @param featureSlot the FeatureSlot to contain this Feature.
     */
    public void setFeatureSlot(FeatureSlot featureSlot) {
        this.strand = featureSlot.getStrand();
        featureSlot.addFeature(this);
    }

    /**
     * Adds a FeatureRange to this Feature.
     *
     * @param featureRange the FeatureRange object to add to this Feature.
     */
    protected void addRange(FeatureRange featureRange) {
        ranges.add(featureRange);
    }

    /**
     * Returns an ArrayList of FeatureRange objects contained by this Feature.
     *
     * @return an ArrayList of FeatureRange objects.
     */
    protected ArrayList getRanges() {
        return ranges;
    }

    /**
     * Sets the default color that will be assigned to FeatureRange objects added to this Feature. This color can be
     * changed for individual FeatureRange objects using {@link FeatureRange#setColor(Color) FeatureRange.setColor()}.
     *
     * @param color the default FeatureRange color.
     */
    public void setColor(Color color) {
        this.color = color;
    }

    /**
     * Returns the default color that will be assigned to FeatureRange objects added to this Feature.
     *
     * @return the default FeatureRange color.
     */
    public Color getColor() {
        return color;
    }

    /**
     * Specifies whether or not labels created for this Feature should be drawn even if they cannot be placed such that
     * they do not clash with other labels. This behaviour can be set for individual FeatureRange objects using {@link
     * FeatureRange#setForceLabel(boolean) FeatureRange.setForceLabel()}.
     *
     * @param forceLabel a boolean specifying whether or not to draw labels for this Feature even if they cannot be
     *                   placed such that they do not clash with other labels.
     */
    public void setForceLabel(boolean forceLabel) {
        this.forceLabel = forceLabel;
        if (this.forceLabel == true) {
            showLabel = LABEL_FORCE;
        }
    }

    /**
     * Returns a boolean specifying whether or not labels created for this Feature should be drawn even if they cannot
     * be placed such that they do not clash with other labels.
     *
     * @return whether or not to draw labels for this Feature even if they cannot be placed such that they do not clash
     *         with other labels.
     */
    public boolean getForceLabel() {
        return forceLabel;
    }

    /**
     * Sets the label text for this Feature. This label will be used in any labels generated for this Feature. This
     * label can be changed for individual FeatureRange objects using {@link FeatureRange#setLabel(String)
     * FeatureRange.setLabel()}.
     *
     * @param label the label for this Feature.
     */
    public void setLabel(String label) {
        this.label = label;
    }

    /**
     * Returns the label text for this Feature. This label will be used in any labels generated for this Feature. This
     * label can be changed for individual featureRanges using {@link FeatureRange#setLabel(String)
     * FeatureRange.setLabel()}.
     *
     * @return the label for this Feature.
     */
    public String getLabel() {
        return label;
    }

    /**
     * Specifies a hyperlink to be associated with this Feature. Hyperlinks are included in SVG output generated using
     * {@link CgviewIO#writeToSVGFile(ca.ualberta.stothard.cgview.Cgview, java.lang.String, boolean, boolean)} or in
     * image maps for PNG and JPG images generated using {@link CgviewIO#writeHTMLFile(ca.ualberta.stothard.cgview.Cgview,
            * java.lang.String, java.lang.String, java.lang.String)}.
     *
     * @param hyperlink a hyperlink for this Feature.
     */
    public void setHyperlink(String hyperlink) {
        this.hyperlink = hyperlink;
    }

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

    /**
     * Specifies a mouseover to be associated with this Feature. Mouseovers are included in SVG output generated using
     * {@link CgviewIO#writeToSVGFile(ca.ualberta.stothard.cgview.Cgview, java.lang.String, boolean, boolean)} or in
     * image maps for PNG and JPG images generated using {@link CgviewIO#writeHTMLFile(ca.ualberta.stothard.cgview.Cgview,
            * java.lang.String, java.lang.String, java.lang.String)}.
     *
     * @param mouseover the mouseover for this feature.
     */
    public void setMouseover(String mouseover) {
        this.mouseover = mouseover;
    }

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

    /**
     * Sets the font used for labels generated for this Feature. This font can be changed for individual FeatureRange
     * objects using {@link FeatureRange#setFont(Font) FeatureRange.setFont()}.
     *
     * @param font the font used for labels generated for this Feature.
     */
    public void setFont(Font font) {
        this.font = font;
    }

    /**
     * Returns the font used for labels generated for this Feature. This font can be changed for individual
     * FeatureRanges using {@link FeatureRange#setFont(Font) FeatureRange.setFont()}.
     *
     * @return the font used for labels generated for this Feature.
     */
    public Font getFont() {
        return font;
    }

    /**
     * Returns the strand of this Feature, which is determined by the FeatureSlot that contains this Feature.
     *
     * @return an <code>int</code> representing the strand of this feature.
     */
    protected int getStrand() {
        return strand;
    }

    /**
     * Sets whether or not a label should be drawn for this Feature. This setting can be changed for individual
     * FeatureRange objects using {@link FeatureRange#setShowLabel(int) FeatureRange.setShowLabel()}.
     *
     * @param showLabel {@link CgviewConstants#LABEL CgviewConstants.LABEL}, {@link CgviewConstants#LABEL_NONE
     *                  CgviewConstants.NO_LABEL}, or {@link CgviewConstants#LABEL_FORCE CgviewConstants.LABEL_FORCE}.
     */
    public void setShowLabel(int showLabel) {
        this.showLabel = showLabel;
        if (this.showLabel == LABEL_FORCE) {
            forceLabel = true;
        } else {
            forceLabel = false;
        }
    }

    /**
     * Returns whether or not a label should be generated for this Feature when drawn. This setting can be changed for
     * individual FeatureRange objects using {@link FeatureRange#setShowLabel(int) FeatureRange.setShowLabel()}.
     *
     * @return {@link CgviewConstants#LABEL CgviewConstants.LABEL}, {@link CgviewConstants#LABEL_NONE
     *         CgviewConstants.NO_LABEL}, or {@link CgviewConstants#LABEL_FORCE CgviewConstants.LABEL_FORCE}.
     */
    public int getShowLabel() {
        return showLabel;
    }

    /**
     * Sets the type of decoration added to this Feature when drawn. This decoration can be changed for individual
     * FeatureRange objects using {@link FeatureRange#setDecoration(int) FeatureRange.setDecoration()}.
     *
     * @param decoration {@link CgviewConstants#DECORATION_STANDARD CgviewConstants.DECORATION_STANDARD}, {@link
     *                   CgviewConstants#DECORATION_COUNTERCLOCKWISE_ARROW CgviewConstants.DECORATION_COUNTERCLOCKWISE_ARROW},
     *                   {@link CgviewConstants#DECORATION_CLOCKWISE_ARROW CgviewConstants.DECORATION_CLOCKWISE_ARROW},
     *                   {@link CgviewConstants#DECORATION_HIDDEN CgviewConstants.DECORATION_HIDDEN}.
     */
    public void setDecoration(int decoration) {
        this.decoration = decoration;
    }

    /**
     * Returns an integer indicating what type of decoration will be added to this Feature when drawn. This decoration
     * can be changed for individual FeatureRange objects using {@link FeatureRange#setDecoration(int)
     * FeatureRange.setDecoration()}.
     *
     * @return {@link CgviewConstants#DECORATION_STANDARD CgviewConstants.DECORATION_STANDARD}, {@link
     *         CgviewConstants#DECORATION_COUNTERCLOCKWISE_ARROW CgviewConstants.DECORATION_COUNTERCLOCKWISE_ARROW},
     *         {@link CgviewConstants#DECORATION_CLOCKWISE_ARROW CgviewConstants.DECORATION_CLOCKWISE_ARROW}, {@link
     *         CgviewConstants#DECORATION_HIDDEN CgviewConstants.DECORATION_HIDDEN}.
     */
    public int getDecoration() {
        return decoration;
    }

    /**
     * Sets the position of this Feature relative to the FeatureSlot object that contains it. This value is only applied
     * when the thickness of this Feature is adjusted using {@link #setProportionOfThickness(float)} so that it is less
     * than 1.0. This setting can be changed for individual FeatureRange objects using {@link
     * FeatureRange#setRadiusAdjustment(float) FeatureRange.setProportionOfThickness()}.
     *
     * @param radiusAdjustment between <code>0</code> and <code>1</code>, with <code>1</code> being near the edge
     *                         furthest from the map center.
     */
    public void setRadiusAdjustment(float radiusAdjustment) {
        if (radiusAdjustment < 0) {
            radiusAdjustment = 0.0f;
        } else if (radiusAdjustment > 1) {
            radiusAdjustment = 1.0f;
        }
        this.radiusAdjustment = radiusAdjustment;
    }

    /**
     * Returns the position of this Feature relative to the FeatureSlot object that contains it. This value is only
     * applied when the thickness of this Feature is adjusted using {@link #setProportionOfThickness(float)} so that it
     * is less than 1.0. This setting can be changed for individual FeatureRange objects using {@link
     * FeatureRange#setRadiusAdjustment(float) FeatureRange.setProportionOfThickness()}.
     *
     * @return a <code>float</code> between <code>0</code> and <code>1</code>, with <code>1</code> being near the edge
     *         furthest from the map center.
     * @see #setProportionOfThickness(float)
     */
    public float getRadiusAdjustment() {
        return radiusAdjustment;
    }

    /**
     * Sets the thickness of this Feature when drawn, as a proportion of the thickness of the FeatureSlot containing
     * this Feature. This setting can be changed for individual FeatureRange objects using {@link
     * FeatureRange#setProportionOfThickness(float) FeatureRange.setProportionOfThickness()}.
     *
     * @param proportionOfThickness between <code>0</code> and <code>1</code>, with <code>1</code> being full
     *                              thickness.
     */
    public void setProportionOfThickness(float proportionOfThickness) {
        if (proportionOfThickness < 0) {
            proportionOfThickness = 0.0f;
        } else if (proportionOfThickness > 1) {
            proportionOfThickness = 1.0f;
        }
        this.proportionOfThickness = proportionOfThickness;
    }

    /**
     * Returns the thickness of this Feature when drawn, as a proportion of the thickness of the FeatureSlot containing
     * this Feature. This setting can be changed for individual FeatureRange objects using {@link
     * FeatureRange#setProportionOfThickness(float) FeatureRange.setProportionOfThickness()}.
     *
     * @return a <code>float</code> between <code>0</code> and <code>1</code>, with <code>1</code> being full
     *         thickness.
     */
    public float getProportionOfThickness() {
        return proportionOfThickness;
    }

    /**
     * Sets the opacity of this Feature when drawn. This setting can be changed for individual FeatureRange objects
     * using {@link FeatureRange#setOpacity(float) FeatureRange.setSwatchOpacity()}.
     *
     * @param opacity the opacity between <code>0</code> and <code>1</code>, with <code>1</code> being the most opaque.
     */
    public void setOpacity(float opacity) {
        if (opacity < 0) {
            opacity = 0.0f;
        } else if (opacity > 1) {
            opacity = 1.0f;
        }
        this.opacity = opacity;
    }

    /**
     * Returns the opacity of this Feature when drawn. This setting can be changed for individual FeatureRange objects
     * using {@link FeatureRange#setOpacity(float) FeatureRange.setSwatchOpacity()}.
     *
     * @return the opacity between <code>0</code> and <code>1</code>, with <code>1</code> being the most opaque.
     */
    public float getOpacity() {
        return opacity;
    }

    /**
     * Sets whether or not this Feature should be drawn with shading.
     *
     * @param showShading whether or not this Feature should be drawn with shading.
     */
    public void setShowShading(boolean showShading) {
        this.showShading = showShading;
    }

    /**
     * Returns whether or not this Feature should be drawn with shading.
     *
     * @return whether or not this Feature should be drawn with shading.
     */
    public boolean getShowShading() {
        return showShading;
    }

    /**
     * Draws this Feature and creates labels if necessary.
     *
     * @param cgview    the Cgview object containing this Feature.
     * @param radius    the radius of the FeatureSlot containing this Feature.
     * @param thickness the thickness of the FeatureSlot containing this Feature.
     */
    protected void draw(Cgview cgview, double radius, float thickness) {
        Iterator i = ranges.iterator();
        while (i.hasNext()) {
            FeatureRange currentFeatureRange = (FeatureRange) i.next();
            currentFeatureRange.draw(cgview, radius, thickness);
            //remove once drawn
            //i.remove();
        }
    }

    /**
     * Returns the smallest start value from the featureRanges in this Feature, or -1 if there are no featureRanges.
     *
     * @return the smallest featureRange start in this featureRange or -1.
     */
    protected int getStart () {

	if (ranges.size() == 0) {
	    return -1;
	}

	Comparator comparator = new SortFeatureRangesByStart();
        Collections.sort(ranges, comparator);

	FeatureRange first = (FeatureRange) ranges.get(0);
	return first.getStart();
	
    }

    //sort so that smallest start is first.
    public class SortFeatureRangesByStart implements Comparator {

	public int compare(Object o1, Object o2) {
	    FeatureRange feature1 = (FeatureRange) o1;
	    FeatureRange feature2 = (FeatureRange) o2;

	    if (feature1.getStart() == feature2.getStart()) {
		return 0;
	    } else if (feature2.getStart() < feature1.getStart()) {
		return 1;
	    } else {
		return -1;
	    }
	}

	public boolean equals(Object obj) {
	    return obj.equals(this);
	}
    }

}
