package ca.ualberta.stothard.cgview;

import java.util.*;

/**
 * Represents a slot of space on the Cgview map. When the map is drawn, the FeatureSlot objects form concentric rings
 * around the backbone circle. Individual features, which are described using {@link Feature} objects, are drawn in the
 * FeatureSlot objects. FeatureSlot objects are allocated space in the order that they are added to the Cgview object,
 * such that those added first receive space closest to the backbone circle.
 *
 * @author Paul Stothard
 */
public class FeatureSlot implements CgviewConstants {

    private int strand;
    private ArrayList featuresInSlot = new ArrayList();
    private double radius = 0.0d;
    private Cgview cgview;
    private float featureThickness;
    private boolean showShading;


    /**
     * Constructs a new FeatureSlot object.
     *
     * @param cgview the Cgview object to contain this FeatureSlot.
     * @param strand the strand of this FeatureSlot, {@link CgviewConstants#DIRECT_STRAND CgviewConstants.DIRECT_STRAND}
     *               or {@link CgviewConstants#REVERSE_STRAND CgviewConstants.REVERSE_STRAND}. If strand is set to
     *               DIRECT_STRAND this FeatureSlot is placed on the outside of the backbone circle. If strand is set to
     *               REVERSE_STRAND this FeatureSlot is placed on the inside of the backbone circle.
     */
    public FeatureSlot(Cgview cgview, int strand) {
        this.strand = strand;
        this.cgview = cgview;
        this.featureThickness = cgview.getFeatureThickness();
        this.showShading = cgview.getShowShading();

        //go through the existing FeatureSlots to determine which position this one will occupy.
        ArrayList featureSlots = cgview.getFeatureSlots();

        //add this FeatureSlot to the Cgview featureSlots.
        featureSlots.add(this);

    }


    /**
     * Constructs a new <code>FeatureSlot</code> object. It can be added to a Cgview object using the setCgview()
     * method.
     *
     * @param strand the strand of this FeatureSlot, {@link CgviewConstants#DIRECT_STRAND CgviewConstants.DIRECT_STRAND}
     *               or {@link CgviewConstants#REVERSE_STRAND CgviewConstants.REVERSE_STRAND}. If strand is set to
     *               DIRECT_STRAND this FeatureSlot is placed on the outside of the backbone circle. If strand is set to
     *               REVERSE_STRAND this FeatureSlot is placed on the inside of the backbone circle.
     */
    public FeatureSlot(int strand) {
        this.strand = strand;
    }

    /**
     * Constructs a new <code>FeatureSlot</code> object. It can be added to a cgview object using the setCgview()
     * method.
     *
     * @param strand      the strand of this featureSlot, {@link CgviewConstants#DIRECT_STRAND
     *                    CgviewConstants.DIRECT_STRAND} or {@link CgviewConstants#REVERSE_STRAND
     *                    CgviewConstants.REVERSE_STRAND}. If strand is set to DIRECT_STRAND this FeatureSlot is placed
     *                    on the outside of the backbone circle. If strand is set to REVERSE_STRAND this FeatureSlot is
     *                    placed on the inside of the backbone circle.
     * @param showShading whether or not items this FeatureSlot should be drawn with shading.
     */
    public FeatureSlot(int strand, boolean showShading) {
        this.strand = strand;
        this.showShading = showShading;
    }

    /**
     * Adds this FeatureSlot to a Cgview object.
     *
     * @param cgview the Cgview object to contain this FeatureSlot.
     */
    public void setCgview(Cgview cgview) {

        this.cgview = cgview;
        this.featureThickness = cgview.getFeatureThickness();
        this.showShading = cgview.getShowShading();

        //go through the existing FeatureSlots to determine which position this one will occupy.
        ArrayList featureSlots = cgview.getFeatureSlots();

        //add this FeatureSlot to the Cgview featureSlots.
        featureSlots.add(this);

    }

    /**
     * Adds a Feature object to this FeatureSlot.
     *
     * @param feature the Feature to add to this FeatureSlot.
     */
    protected void addFeature(Feature feature) {
        featuresInSlot.add(feature);
    }

    /**
     * Returns the strand of this FeatureSlot.
     *
     * @return an <code>int</code> representing the strand of this FeatureSlot.
     */
    public int getStrand() {
        return strand;
    }

    /**
     * Sets the thickness of the arc used to represent sequence features in this FeatureSlot.
     *
     * @param featureThickness the feature thickness.
     */
    public void setFeatureThickness(float featureThickness) {
        if (featureThickness < 0) {
            featureThickness = 0.0f;
        }
        this.featureThickness = featureThickness;
    }

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

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

    /**
     * Returns the thickness of the arc used to represent sequence features in this FeatureSlot.
     *
     * @return the feature thickness.
     */
    public float getFeatureThickness() {
        return featureThickness;
    }

    /**
     * Sets the radius of this FeatureSlot.
     */
    protected void setRadius() {
        if (strand == DIRECT_STRAND) {
            radius = cgview.getFirstOuterFeatureRadius();
            ArrayList featureSlots = cgview.getFeatureSlots();
            Iterator i = featureSlots.iterator();
            while (i.hasNext()) {
                FeatureSlot currentFeatureSlot = (FeatureSlot) i.next();
                if (currentFeatureSlot.getStrand() == DIRECT_STRAND) {
                    if (currentFeatureSlot.equals(this)) {
                        radius = radius + 0.5d * featureThickness;
                        break;
                    } else {
                        radius = radius + currentFeatureSlot.getFeatureThickness() + cgview.getFeatureSlotSpacing();
                    }
                }
            }
        } else {
            radius = cgview.getFirstInnerFeatureRadius();
            ArrayList featureSlots = cgview.getFeatureSlots();
            Iterator i = featureSlots.iterator();
            while (i.hasNext()) {
                FeatureSlot currentFeatureSlot = (FeatureSlot) i.next();
                if (currentFeatureSlot.getStrand() == REVERSE_STRAND) {
                    if (currentFeatureSlot.equals(this)) {
                        radius = radius - 0.5d * featureThickness;
                        break;
                    } else {
                        radius = radius - currentFeatureSlot.getFeatureThickness() - cgview.getFeatureSlotSpacing();
                    }
                }
            }
        }
    }

    /**
     * Returns the minimum feature length in bases that can be drawn accurately in this FeatureSlot, at the given zoom
     * value. Feature lengths smaller than this length are automatically drawn larger so that they are visible. This
     * method should be used when deciding on the appropriate window size (length of bases) to use when calculating a
     * value to present on the map. For example, if you plan to display percent GC content on the map, and this method
     * returns <code>98.0003</code>, you should choose a window size of 99 or greater when calculating percent GC. In
     * other words, you would create a Feature object containing a FeatureRange object spanning bases 1 to 99, a second
     * FeatureRange spanning bases 100 to 199, and so on. Each FeatureRange would be decorated in some way to convey the
     * %GC value calculated for the segment of bases it contains (using {@link FeatureRange#setProportionOfThickness(float)}
     * for example).
     *
     * @param zoom the zoom value to be used when drawing the Cgview map.
     */
    public double getBasesPerMinFeature(double zoom) {
        this.setRadius();
        double adjustedZoom = cgview.adjustZoom(zoom);
        double minimumFeatureLength = cgview.getMinimumFeatureLength();
        double basePerCircum = (double) (cgview.getSequenceLength()) / (2.0d * Math.PI * radius * adjustedZoom);
        return minimumFeatureLength * basePerCircum;
    }

    /**
     * Draws the contents of this FeatureSlot.
     */
    protected void draw() {
        this.setRadius();
        Iterator i = featuresInSlot.iterator();
        while (i.hasNext()) {
            Feature currentFeature = (Feature) i.next();
            currentFeature.draw(cgview, radius, featureThickness);
            //remove once drawn
            //i.remove();
        }
    }

    /**
     * Returns a boolean specifying whether or not the supplied Feature object can fit in this FeatureSlot without
     * overlapping with Feature objects already present in this FeatureSlot.
     *
     * @param feature the Feature to check.
     * @return whether or not the Feature object can fit without overlapping with other Feature objects.
     */
    public boolean isRoom(Feature feature) {
        //go through each feature and each featureRange.
        Iterator i = featuresInSlot.iterator();
        while (i.hasNext()) {
            Feature currentFeature = (Feature) i.next();
            Iterator j = currentFeature.getRanges().iterator();

            while (j.hasNext()) {
                FeatureRange currentFeatureRange = (FeatureRange) j.next();
                if (currentFeatureRange.getDecoration() == DECORATION_HIDDEN) {
                    continue;
                }

                int startOne = currentFeatureRange.getStart();
                int stopOne = currentFeatureRange.getStop();

                //now examine the featureRanges in the submitted feature.
                Iterator k = feature.getRanges().iterator();

                while (k.hasNext()) {
                    FeatureRange innerFeatureRange = (FeatureRange) k.next();

                    if (innerFeatureRange.getDecoration() == DECORATION_HIDDEN) {
                        continue;
                    }

                    int startTwo = innerFeatureRange.getStart();
                    int stopTwo = innerFeatureRange.getStop();

                    if ((startOne < stopOne) && (startTwo < stopTwo)) {
                        if ((startTwo > stopOne) || (stopTwo < startOne)) {
                            continue;
                        } else {
                            //System.out.println ("label overlap 1: " + startOne + "-" + stopOne + ", " + startTwo + "-" + stopTwo + ".");
                            return false;
                        }
                    } else if ((startOne > stopOne) && (startTwo > stopTwo)) {
                        //System.out.println ("label overlap 2: " + startOne + "-" + stopOne + ", " + startTwo + "-" + stopTwo + ".");
                        return false;
                    } else if ((startOne < stopOne) && (startTwo > stopOne) && (stopTwo < startOne)) {
                        continue;
                    } else if ((startTwo < stopTwo) && (startOne > stopTwo) && (stopOne < startTwo)) {
                        continue;
                    } else {
                        //System.out.println ("label overlap 3: " + startOne + "-" + stopOne + ", " + startTwo + "-" + stopTwo + ".");
                        return false;
                    }
                }
            }// end of while

        }
        return true;
    }

    /**
     * Sorts the features in this FeatureSlot by start base. If this is a DIRECT_STRAND slot the features
     * are sorted in ascending order. If this is a REVERSE_STRAND slot the features are sorted in 
     * descending order. This sorting produces a more visually pleasing feature appearance when arrows are
     * used.
     */
    public void sortFeaturesByStart() {

	Comparator comparator = new SortFeaturesByStart();
        Collections.sort(featuresInSlot, comparator);

	if (this.strand == DIRECT_STRAND) {
	    Collections.reverse(featuresInSlot);
	}
    }

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

	public int compare(Object o1, Object o2) {
	    Feature feature1 = (Feature) o1;
	    Feature feature2 = (Feature) 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);
	}
    }

}
