//------------------------------------------------------------------------------------------------//
//                                                                                                //
//                                        H e a d I n t e r                                       //
//                                                                                                //
//------------------------------------------------------------------------------------------------//
// <editor-fold defaultstate="collapsed" desc="hdr">
//
//  Copyright © Audiveris 2018. All rights reserved.
//
//  This program is free software: you can redistribute it and/or modify it under the terms of the
//  GNU Affero General Public License as published by the Free Software Foundation, either version
//  3 of the License, or (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
//  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//  See the GNU Affero General Public License for more details.
//
//  You should have received a copy of the GNU Affero General Public License along with this
//  program.  If not, see <http://www.gnu.org/licenses/>.
//------------------------------------------------------------------------------------------------//
// </editor-fold>
package org.audiveris.omr.sig.inter;

import ij.process.ByteProcessor;

import org.audiveris.omr.constant.Constant;
import org.audiveris.omr.constant.ConstantSet;
import org.audiveris.omr.glyph.Glyph;
import org.audiveris.omr.glyph.Shape;
import org.audiveris.omr.glyph.ShapeSet;
import org.audiveris.omr.image.Anchored.Anchor;
import org.audiveris.omr.image.ShapeDescriptor;
import org.audiveris.omr.image.Template;
import org.audiveris.omr.image.TemplateFactory;
import org.audiveris.omr.math.GeoOrder;
import org.audiveris.omr.math.LineUtil;
import org.audiveris.omr.math.PointUtil;
import static org.audiveris.omr.run.Orientation.VERTICAL;
import org.audiveris.omr.run.RunTable;
import org.audiveris.omr.run.RunTableFactory;
import org.audiveris.omr.sheet.Scale;
import org.audiveris.omr.sheet.Sheet;
import org.audiveris.omr.sheet.Staff;
import org.audiveris.omr.sheet.SystemInfo;
import org.audiveris.omr.sheet.rhythm.Measure;
import org.audiveris.omr.sig.GradeImpacts;
import org.audiveris.omr.sig.relation.AlterHeadRelation;
import org.audiveris.omr.sig.relation.HeadStemRelation;
import org.audiveris.omr.sig.relation.Link;
import org.audiveris.omr.sig.relation.Relation;
import org.audiveris.omr.sig.relation.SlurHeadRelation;
import org.audiveris.omr.util.ByteUtil;
import org.audiveris.omr.util.Corner;
import org.audiveris.omr.util.HorizontalSide;
import static org.audiveris.omr.util.HorizontalSide.*;
import static org.audiveris.omr.util.VerticalSide.*;
import org.audiveris.omr.util.Jaxb;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

/**
 * Class {@code HeadInter} represents a note head, that is any head shape including
 * whole and breve, but not a rest.
 * <p>
 * These rather round-shaped symbols are retrieved via template-matching technique.
 *
 * @author Hervé Bitteur
 */
@XmlRootElement(name = "head")
@XmlAccessorType(XmlAccessType.NONE)
public class HeadInter
        extends AbstractNoteInter
{

    private static final Constants constants = new Constants();

    private static final Logger logger = LoggerFactory.getLogger(HeadInter.class);

    // Persistent data
    //----------------
    //
    /** Absolute location of head template pivot. */
    @XmlElement
    @XmlJavaTypeAdapter(Jaxb.PointAdapter.class)
    private final Point pivot;

    /** Relative pivot position WRT head. */
    @XmlAttribute
    private final Anchor anchor;

    // Transient data
    //---------------
    //
    /** Shape template descriptor. */
    private ShapeDescriptor descriptor;

    /**
     * Creates a new {@code HeadInter} object.
     *
     * @param pivot   the template pivot
     * @param anchor  relative pivot configuration
     * @param bounds  the object bounds
     * @param shape   the underlying shape
     * @param impacts the grade details
     * @param staff   the related staff
     * @param pitch   the note pitch
     */
    public HeadInter (Point pivot,
                      Anchor anchor,
                      Rectangle bounds,
                      Shape shape,
                      GradeImpacts impacts,
                      Staff staff,
                      Double pitch)
    {
        super(null, bounds, shape, impacts, staff, pitch);
        this.pivot = pivot;
        this.anchor = anchor;
    }

    /**
     * Creates a new {@code HeadInter} object.
     *
     * @param pivot  the template pivot
     * @param anchor relative pivot configuration
     * @param bounds the object bounds
     * @param shape  the underlying shape
     * @param grade  quality grade
     * @param staff  the related staff
     * @param pitch  the note pitch
     */
    public HeadInter (Point pivot,
                      Anchor anchor,
                      Rectangle bounds,
                      Shape shape,
                      double grade,
                      Staff staff,
                      Double pitch)
    {
        super(null, bounds, shape, grade, staff, pitch);
        this.pivot = pivot;
        this.anchor = anchor;
    }

    /** No-arg constructor needed by JAXB. */
    private HeadInter ()
    {
        this.pivot = null;
        this.anchor = null;
    }

    //--------//
    // accept //
    //--------//
    @Override
    public void accept (InterVisitor visitor)
    {
        visitor.visit(this);
    }

    //-------//
    // added //
    //-------//
    @Override
    public void added ()
    {
        super.added();

        if (ShapeSet.StemHeads.contains(shape)) {
            setAbnormal(true); // No stem linked yet
        }
    }

    //---------------//
    // checkAbnormal //
    //---------------//
    @Override
    public boolean checkAbnormal ()
    {
        if (ShapeSet.StemHeads.contains(shape)) {
            // Check if a stem is connected
            setAbnormal(!sig.hasRelation(this, HeadStemRelation.class));
        }

        return isAbnormal();
    }

    //----------//
    // contains //
    //----------//
    @Override
    public boolean contains (Point point)
    {
        if (!super.contains(point)) {
            return false;
        }

        Line2D midLine = getMidLine();

        if (midLine != null) {
            return midLine.relativeCCW(point) < 0;
        }

        return true;
    }

    //-----------//
    // duplicate //
    //-----------//
    /**
     * Duplicate this head, keeping the same head shape (black vs void).
     *
     * @return a duplicate head
     */
    public HeadInter duplicate ()
    {
        return duplicateAs(shape);
    }

    //-------------//
    // duplicateAs //
    //-------------//
    /**
     * Build a duplicate of this head, perhaps with a different shape.
     * <p>
     * Used when say a half head is shared, with flag/beam on one side.
     *
     * @param shape precise shape for the duplicate
     * @return duplicate
     */
    public HeadInter duplicateAs (Shape shape)
    {
        HeadInter clone = new HeadInter(pivot, anchor, bounds, shape, impacts, staff, pitch);

        clone.setGlyph(this.glyph);

        if (impacts == null) {
            clone.setGrade(grade);
        }

        return clone;
    }

    //---------------//
    // getAccidental //
    //---------------//
    /**
     * Report the (local) accidental, if any, related to this head.
     *
     * @return the related accidental, or null
     */
    public AlterInter getAccidental ()
    {
        for (Relation rel : sig.getRelations(this, AlterHeadRelation.class)) {
            return (AlterInter) sig.getOppositeInter(this, rel);
        }

        return null;
    }

    //---------------//
    // getAlteration //
    //---------------//
    /**
     * Report the actual alteration of this note, taking into account the accidental of
     * this note if any, the accidental of previous note with same step within the same
     * measure, a tie from previous measure and finally the current key signature.
     *
     * @param fifths fifths value for current key signature
     * @return the actual alteration
     */
    public int getAlteration (Integer fifths)
    {
        return HeadInter.this.getAlteration(fifths, true);
    }

    //---------------//
    // getAlteration //
    //---------------//
    /**
     * Report the actual alteration of this note, taking into account the accidental of
     * this note if any, the accidental of previous note with same step within the same
     * measure, a tie from previous measure and finally the current key signature.
     *
     * @param fifths fifths value for current key signature
     * @param useTie true to use tie for check
     * @return the actual alteration
     */
    public int getAlteration (Integer fifths,
                              boolean useTie)
    {
        // Look for local/measure accidental
        AlterInter accidental = getMeasureAccidental();

        if (accidental != null) {
            return AlterInter.alterationOf(accidental);
        }

        if (useTie) {
            // Look for tie from previous measure (same system or previous system)
            for (Relation rel : sig.getRelations(this, SlurHeadRelation.class)) {
                SlurInter slur = (SlurInter) sig.getOppositeInter(this, rel);

                if (slur.isTie() && (slur.getHead(HorizontalSide.RIGHT) == this)) {
                    // Is the starting head in same system?
                    HeadInter startHead = slur.getHead(HorizontalSide.LEFT);

                    if (startHead != null) {
                        // Use start head alter
                        return startHead.getAlteration(fifths);
                    }

                    // Use slur extension to look into previous system
                    SlurInter prevSlur = slur.getExtension(HorizontalSide.LEFT);

                    if (prevSlur != null) {
                        startHead = prevSlur.getHead(HorizontalSide.LEFT);

                        if (startHead != null) {
                            // Use start head alter
                            return startHead.getAlteration(fifths);
                        }
                    }

                    // TODO: Here we should look in previous sheet/page...
                }
            }
        }

        // Finally, use the current key signature
        if (fifths != null) {
            return KeyInter.getAlterFor(getStep(), fifths);
        }

        // Nothing found, so...
        return 0;
    }

    //----------------------//
    // getMeasureAccidental //
    //----------------------//
    /**
     * Report the accidental (if any) which applies to this head or to a previous
     * compatible one in the same measure.
     *
     * @return the measure scoped accidental found or null
     */
    public AlterInter getMeasureAccidental ()
    {
        // Look for local accidental
        AlterInter accidental = getAccidental();

        if (accidental != null) {
            return accidental;
        }

        // Look for previous accidental with same note step in the same measure
        // Let's avoid the use of time slots (which would require RHYTHMS step to be done!)
        Measure measure = getChord().getMeasure();
        List<Inter> heads = new ArrayList<>();

        for (HeadChordInter headChord : measure.getHeadChords()) {
            heads.addAll(headChord.getMembers());
        }

        boolean started = false;
        Collections.sort(heads, Inters.byReverseCenterAbscissa);

        for (Inter inter : heads) {
            HeadInter head = (HeadInter) inter;

            if (head == this) {
                started = true;
            } else if (started && (head.getStep() == getStep())
                               && (head.getOctave() == getOctave())
                               && (head.getStaff() == getStaff())) {
                accidental = head.getAccidental();

                if (accidental != null) {
                    return accidental;
                }
            }
        }

        return null;
    }

    //----------//
    // getChord //
    //----------//
    /**
     * Report the containing (head) chord, if any.
     *
     * @return containing chord or null
     */
    @Override
    public HeadChordInter getChord ()
    {
        return (HeadChordInter) getEnsemble();
    }

    //---------------//
    // getCoreBounds //
    //---------------//
    @Override
    public Rectangle2D getCoreBounds ()
    {
        return shrink(getBounds());
    }

    //---------------//
    // getDescriptor //
    //---------------//
    /**
     * Report the descriptor used to generate this head shape with proper size.
     *
     * @return related template descriptor
     */
    public ShapeDescriptor getDescriptor ()
    {
        if (descriptor == null) {
            final int pointSize = staff.getHeadPointSize();
            descriptor = TemplateFactory.getInstance().getCatalog(pointSize).getDescriptor(shape);
        }

        return descriptor;
    }

    //------------//
    // getMidLine //
    //------------//
    /**
     * Report the separating line for shared heads.
     * <p>
     * The line is nearly vertical, oriented from head to stem.
     * Thus, the relativeCCW is negative for points located on proper head half.
     *
     * @return oriented middle line or null if not mirrored
     */
    public Line2D getMidLine ()
    {
        if (getMirror() == null) {
            return null;
        }

        final Rectangle box = getBounds();

        for (Relation relation : sig.getRelations(this, HeadStemRelation.class)) {
            final HeadStemRelation rel = (HeadStemRelation) relation;

            if (rel.getHeadSide() == HorizontalSide.LEFT) {
                return LineUtil.bisector(
                        new Point(box.x, box.y + box.height),
                        new Point(box.x + box.width, box.y));
            } else {
                return LineUtil.bisector(
                        new Point(box.x + box.width, box.y),
                        new Point(box.x, box.y + box.height));
            }
        }

        return null;
    }

    //-------------------//
    // getRelationCenter //
    //-------------------//
    /**
     * {@inheritDoc}
     * <p>
     * For shared heads, the relation center is slightly shifted to the containing chord.
     *
     * @return the head relation center, shifted for a shared head
     */
    @Override
    public Point getRelationCenter ()
    {
        final Point center = getCenter();

        if (getMirror() == null) {
            return center;
        }

        final Rectangle box = getBounds();
        final int dx = box.width / 5;
        final int dy = box.height / 5;

        for (Relation relation : sig.getRelations(this, HeadStemRelation.class)) {
            final HeadStemRelation rel = (HeadStemRelation) relation;

            if (rel.getHeadSide() == HorizontalSide.LEFT) {
                center.translate(-dx, +dy);
            } else {
                center.translate(+dx, -dy);
            }

            return center;
        }

        return center; // Should not occur...
    }

    //--------------//
    // getSideStems //
    //--------------//
    /**
     * Report the stems linked to this head, organized by head side.
     *
     * @return the map of linked stems, organized by head side
     * @see #getStems()
     */
    public Map<HorizontalSide, Set<StemInter>> getSideStems ()
    {
        // Split connected stems into left and right sides
        final Map<HorizontalSide, Set<StemInter>> map = new EnumMap<>(HorizontalSide.class);

        for (Relation relation : sig.getRelations(this, HeadStemRelation.class)) {
            HeadStemRelation rel = (HeadStemRelation) relation;
            HorizontalSide side = rel.getHeadSide();
            Set<StemInter> set = map.get(side);

            if (set == null) {
                map.put(side, set = new LinkedHashSet<>());
            }

            set.add((StemInter) sig.getEdgeTarget(rel));
        }

        return map;
    }

    //-----------------------//
    // getStemReferencePoint //
    //-----------------------//
    /**
     * Report the reference point for a stem connection.
     *
     * @param anchor    desired side for stem (typically TOP_RIGHT_STEM or BOTTOM_LEFT_STEM)
     * @param interline relevant interline value
     * @return the reference point
     */
    public Point2D getStemReferencePoint (Anchor anchor,
                                          int interline)
    {
        ShapeDescriptor desc = getDescriptor();
        Rectangle templateBox = desc.getBounds(this.getBounds());
        Point ref = templateBox.getLocation();
        Point offset = desc.getOffset(anchor);
        ref.translate(offset.x, offset.y);

        return ref;
    }

    //----------//
    // getStems //
    //----------//
    /**
     * Report the stems linked to this head, whatever the side.
     *
     * @return set of linked stems
     * @see #getSideStems()
     */
    public Set<StemInter> getStems ()
    {
        final Set<StemInter> set = new LinkedHashSet<>();

        for (Relation relation : sig.getRelations(this, HeadStemRelation.class)) {
            set.add((StemInter) sig.getEdgeTarget(relation));
        }

        return set;
    }

    //----------//
    // overlaps //
    //----------//
    /**
     * Precise overlap implementation between notes, based on their pitch value.
     * <p>
     * TODO: A clean overlap check might use true distance tables around each of the heads.
     * For the time being, we simply play with the width and area of intersection rectangle.
     *
     * @param that another inter (perhaps a note)
     * @return true if overlap is detected
     * @throws DeletedInterException when an Inter instance no longer exists in its SIG
     */
    @Override
    public boolean overlaps (Inter that)
            throws DeletedInterException
    {
        // Specific between notes
        if (that instanceof HeadInter) {
            if (this.isVip() && ((HeadInter) that).isVip()) {
                logger.info("HeadInter checking overlaps between {} and {}", this, that);
            }

            HeadInter thatHead = (HeadInter) that;

            // Check vertical distance
            if (this.getStaff() == that.getStaff()) {
                if (Math.abs(thatHead.getIntegerPitch() - getIntegerPitch()) > 1) {
                    return false;
                }
            } else {
                // We have two note heads from different staves and with overlapping bounds!
                fixDuplicateWith(thatHead); // Throws DeletedInterException when fixed

                return true;
            }

            // Check horizontal distance
            Rectangle thisBounds = this.getBounds();
            Rectangle thatBounds = thatHead.getBounds();
            Rectangle common = thisBounds.intersection(thatBounds);

            if (common.width <= 0) {
                return false;
            }

            int thisArea = thisBounds.width * thisBounds.height;
            int thatArea = thatBounds.width * thatBounds.height;
            int minArea = Math.min(thisArea, thatArea);
            int commonArea = common.width * common.height;
            double areaRatio = (double) commonArea / minArea;
            boolean res = (common.width > (constants.maxOverlapDxRatio.getValue()
                                                   * thisBounds.width)) && (areaRatio
                                                                                    > constants.maxOverlapAreaRatio
                            .getValue());

            return res;

            //        } else if (that instanceof StemInter) {
            //            // Head with respect to a stem
            //            // First, standard check
            //            if (!Glyphs.intersect(this.getGlyph(), that.getGlyph(), false)) {
            //                return false;
            //            }
            //
            //            // Second, limit stem vertical range to connection points of ending heads if any
            //            // (Assuming wrong-side ending heads have been pruned beforehand)
            //            StemInter stem = (StemInter) that;
            //            Line2D line = stem.computeAnchoredLine();
            //            int top = (int) Math.ceil(line.getY1());
            //            int bottom = (int) Math.floor(line.getY2());
            //            Rectangle box = stem.getBounds();
            //            Rectangle anchorRect = new Rectangle(box.x, top, box.width, bottom - top + 1);
            //
            //            return this.getCoreBounds().intersects(anchorRect);
        }

        // Basic test
        return super.overlaps(that);
    }

    //---------------//
    // retrieveGlyph //
    //---------------//
    /**
     * Use descriptor to build an underlying glyph.
     *
     * @param image the image to read pixels from
     * @return the underlying glyph or null if failed
     */
    public Glyph retrieveGlyph (ByteProcessor image)
    {
        getDescriptor();

        final Sheet sheet = staff.getSystem().getSheet();
        final Template tpl = descriptor.getTemplate();
        final Rectangle interBox = getBounds();
        final Rectangle descBox = descriptor.getBounds(interBox);

        // Foreground points (coordinates WRT descBox)
        final List<Point> fores = tpl.getForegroundPixels(descBox, image);

        if (fores.isEmpty()) {
            logger.info("No foreground pixels for {}", this);

            return null;
        }

        final Rectangle foreBox = PointUtil.boundsOf(fores);

        final ByteProcessor buf = new ByteProcessor(foreBox.width, foreBox.height);
        ByteUtil.raz(buf);

        for (Point p : fores) {
            buf.set(p.x - foreBox.x, p.y - foreBox.y, 0);
        }

        // Runs
        RunTable runTable = new RunTableFactory(VERTICAL).createTable(buf);

        // Glyph
        glyph = sheet.getGlyphIndex().registerOriginal(
                new Glyph(descBox.x + foreBox.x, descBox.y + foreBox.y, runTable));

        // Use glyph bounds as inter bounds
        bounds = glyph.getBounds();

        return glyph;
    }

    //-------------//
    // searchLinks //
    //-------------//
    /**
     * {@inheritDoc}
     * <p>
     * Specifically, look for stem to allow head attachment.
     *
     * @return stem link, perhaps empty
     */
    @Override
    public Collection<Link> searchLinks (SystemInfo system,
                                         boolean doit)
    {
        if (ShapeSet.StemHeads.contains(shape)) {
            // Not very optimized!
            List<Inter> systemStems = system.getSig().inters(StemInter.class);
            Collections.sort(systemStems, Inters.byAbscissa);

            Link link = lookupLink(systemStems);

            if (link != null) {
                if (doit) {
                    link.applyTo(this);
                }

                return Collections.singleton(link);
            }
        }

        return Collections.emptyList();
    }

    //-----------//
    // internals //
    //-----------//
    @Override
    protected String internals ()
    {
        return super.internals() + " " + shape;
    }

    //------------------//
    // fixDuplicateWith //
    //------------------//
    /**
     * Fix head duplication on two staves.
     * <p>
     * We have two note heads from different staves and with overlapping bound.
     * Vertical gap between the staves must be small and crowded, leading to head being "duplicated"
     * in both staves.
     * <p>
     * Assuming there is a linked stem, we could use sibling stem/head in a beam group if any.
     * Or we can simply use stem direction, assumed to point to the "true" containing staff.
     *
     * @param that the other inter
     */
    private void fixDuplicateWith (HeadInter that)
            throws DeletedInterException
    {
        for (Relation rel : sig.getRelations(this, HeadStemRelation.class)) {
            StemInter thisStem = (StemInter) sig.getOppositeInter(this, rel);
            int thisDir = thisStem.computeDirection();
            Inter dupli = ((thisDir * (that.getStaff().getId() - this.getStaff().getId())) > 0)
                    ? this : that;

            logger.debug("Deleting duplicated {}", dupli);
            dupli.remove();
            throw new DeletedInterException(dupli);
        }

        //TODO: What if we have no stem? It's a WHOLE_NOTE or SMALL_WHOLE_NOTE
        // Perhaps check for a weak ledger, tangent to the note towards staff
    }

    //------------//
    // lookupLink //
    //------------//
    /**
     * Try to detect a link between this Head instance and a stem nearby.
     * <p>
     * 1/ Use a lookup area on each horizontal side of the head to filter candidate stems.
     * 2/ Select the best connection among the compatible candidates.
     *
     * @param systemStems abscissa-ordered collection of stems in system
     * @return the link found or null
     */
    private Link lookupLink (List<Inter> systemStems)
    {
        if (systemStems.isEmpty()) {
            return null;
        }

        final SystemInfo system = systemStems.get(0).getSig().getSystem();
        final Scale scale = system.getSheet().getScale();
        final int interline = scale.getInterline();
        final int maxHeadInDx = scale.toPixels(HeadStemRelation.getXInGapMaximum(manual));
        final int maxHeadOutDx = scale.toPixels(HeadStemRelation.getXOutGapMaximum(manual));
        final int maxYGap = scale.toPixels(HeadStemRelation.getYGapMaximum(manual));

        Link bestLink = null;
        double bestGrade = 0;

        for (Corner corner : Corner.values) {
            Point refPt = PointUtil.rounded(getStemReferencePoint(corner.stemAnchor(), interline));
            int xMin = refPt.x - ((corner.hSide == RIGHT) ? maxHeadInDx : maxHeadOutDx);
            int yMin = refPt.y - ((corner.vSide == TOP) ? maxYGap : 0);
            Rectangle luBox = new Rectangle(xMin, yMin, maxHeadInDx + maxHeadOutDx, maxYGap);
            List<Inter> stems = Inters.intersectedInters(systemStems, GeoOrder.BY_ABSCISSA, luBox);
            int xDir = (corner.hSide == RIGHT) ? 1 : (-1);

            for (Inter inter : stems) {
                StemInter stem = (StemInter) inter;
                final Point2D start = stem.getTop();
                final Point2D stop = stem.getBottom();

                double crossX = LineUtil.xAtY(start, stop, refPt.getY());
                final double xGap = xDir * (crossX - refPt.getX());
                final double yGap;

                if (refPt.getY() < start.getY()) {
                    yGap = start.getY() - refPt.getY();
                } else if (refPt.getY() > stop.getY()) {
                    yGap = refPt.getY() - stop.getY();
                } else {
                    yGap = 0;
                }

                HeadStemRelation rel = new HeadStemRelation();
                rel.setInOutGaps(scale.pixelsToFrac(xGap), scale.pixelsToFrac(yGap), manual);

                if (rel.getGrade() >= rel.getMinGrade()) {
                    if ((bestLink == null) || (rel.getGrade() > bestGrade)) {
                        rel.setExtensionPoint(refPt); // Approximately
                        bestLink = new Link(stem, rel, true);
                        bestGrade = rel.getGrade();
                    }
                }
            }
        }

        return bestLink;
    }

    //--------------------//
    // getShrinkHoriRatio //
    //--------------------//
    /**
     * Report horizontal ratio to check overlap
     *
     * @return horizontal ratio
     */
    public static double getShrinkHoriRatio ()
    {
        return constants.shrinkHoriRatio.getValue();
    }

    //--------------------//
    // getShrinkVertRatio //
    //--------------------//
    /**
     * Report vertical ratio to check overlap
     *
     * @return vertical ratio
     */
    public static double getShrinkVertRatio ()
    {
        return constants.shrinkVertRatio.getValue();
    }

    //--------//
    // shrink //
    //--------//
    /**
     * Shrink a bit a bounding bounds when checking for note overlap.
     *
     * @param box the bounding bounds
     * @return the shrunk bounds
     */
    public static Rectangle2D shrink (Rectangle box)
    {
        double newWidth = constants.shrinkHoriRatio.getValue() * box.width;
        double newHeight = constants.shrinkVertRatio.getValue() * box.height;

        return new Rectangle2D.Double(
                box.getCenterX() - (newWidth / 2.0),
                box.getCenterY() - (newHeight / 2.0),
                newWidth,
                newHeight);
    }

    //---------//
    // Impacts //
    //---------//
    public static class Impacts
            extends GradeImpacts
    {

        private static final String[] NAMES = new String[]{"dist"};

        private static final double[] WEIGHTS = new double[]{1};

        public Impacts (double dist)
        {
            super(NAMES, WEIGHTS);
            setImpact(0, dist);
        }
    }

    //-----------//
    // Constants //
    //-----------//
    private static class Constants
            extends ConstantSet
    {

        private final Constant.Ratio shrinkHoriRatio = new Constant.Ratio(
                0.5,
                "Horizontal shrink ratio to apply when checking note overlap");

        private final Constant.Ratio shrinkVertRatio = new Constant.Ratio(
                0.5,
                "Vertical shrink ratio to apply when checking note overlap");

        private final Constant.Ratio maxOverlapDxRatio = new Constant.Ratio(
                0.2,
                "Maximum acceptable abscissa overlap ratio between notes");

        private final Constant.Ratio maxOverlapAreaRatio = new Constant.Ratio(
                0.25,
                "Maximum acceptable box area overlap ratio between notes");
    }
}