//------------------------------------------------------------------------------------------------//
//                                                                                                //
//                                           S I G r a p h                                        //
//                                                                                                //
//------------------------------------------------------------------------------------------------//
// <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;

import org.audiveris.omr.glyph.Grades;
import org.audiveris.omr.glyph.Shape;
import org.audiveris.omr.sheet.Staff;
import org.audiveris.omr.sheet.SystemInfo;
import org.audiveris.omr.sig.inter.AbstractInter;
import org.audiveris.omr.sig.inter.HeadInter;
import org.audiveris.omr.sig.inter.Inter;
import org.audiveris.omr.sig.inter.Inters;
import org.audiveris.omr.sig.inter.Inters.ClassPredicate;
import org.audiveris.omr.sig.inter.Inters.ClassesPredicate;
import org.audiveris.omr.sig.inter.StemInter;
import org.audiveris.omr.sig.relation.Exclusion;
import org.audiveris.omr.sig.relation.Exclusion.Cause;
import org.audiveris.omr.sig.relation.Relation;
import org.audiveris.omr.sig.relation.Support;
import org.audiveris.omr.util.Navigable;
import org.audiveris.omr.util.Predicate;

import org.jgrapht.DirectedGraph;
import org.jgrapht.Graphs;
import org.jgrapht.graph.DefaultListenableGraph;
import org.jgrapht.graph.DirectedMultigraph;

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

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

/**
 * Class {@code SIGraph} represents the Symbol Interpretation Graph that aims at
 * finding the best global interpretation of all symbols in a system.
 *
 * @author Hervé Bitteur
 */
@XmlJavaTypeAdapter(SigValue.Adapter.class)
public class SIGraph
        extends DefaultListenableGraph<Inter, Relation>
        implements DirectedGraph<Inter, Relation>
{

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

    /** Dedicated system. */
    @Navigable(false)
    private SystemInfo system;

    /** Content for differed populating after unmarshalling. */
    private SigValue sigValue;

    /**
     * Creates a new SIGraph object at system level.
     *
     * @param system the containing system
     */
    public SIGraph (SystemInfo system)
    {
        super(new DirectedMultigraph(Relation.class), true /* reuseEvents */);

        Objects.requireNonNull(system, "A sig needs a non-null system");
        this.system = system;
    }

    /**
     * Special creation of a SIG to be later populated via the provided SigValue.
     *
     * @param sigValue the SIG content, with IDREFs not yet filled
     */
    SIGraph (SigValue sigValue)
    {
        this();
        this.sigValue = sigValue;
    }

    /**
     * No-arg constructor meant for JAXB.
     */
    private SIGraph ()
    {
        super(new DirectedMultigraph(Relation.class), true /* reuseEvents */);
    }

    //-----------//
    // addVertex //
    //-----------//
    /**
     * {@inheritDoc}
     * <p>
     * Overridden so that all interpretations keep a pointer to their hosting sig.
     *
     * @param inter the brand new interpretation
     * @return true if the inter was actually added, false if it existed before
     */
    @Override
    public boolean addVertex (Inter inter)
    {
        // Update index
        if (inter.getId() == 0) {
            system.getSheet().getInterIndex().register(inter);
        } else {
            system.getSheet().getInterIndex().insert(inter);
        }

        // Update sig
        boolean added = super.addVertex(inter);

        if (added) {
            inter.setSig(this);

            // Additional actions
            inter.added();
        }

        return added;
    }

    //-------------//
    // afterReload //
    //-------------//
    /**
     * Complete SIG reload now that it's safe to use the (fully unmarshalled) sigValue.
     *
     * @param system the system for this sig
     */
    public void afterReload (SystemInfo system)
    {
        try {
            this.system = system;

            sigValue.populateSig(this);

            // SigValue is no longer useful and can be disposed of
            sigValue = null;

            upgradeInters(); // Temporary upgrade stuff

        } catch (Exception ex) {
            logger.warn("Error in " + getClass() + " afterReload() " + ex, ex);
        }
    }

    //------------------------//
    // computeContextualGrade //
    //------------------------//
    /**
     * Compute the contextual grade of provided inter.
     *
     * @param inter provided inter
     * @return contextual grade value
     */
    public double computeContextualGrade (Inter inter)
    {
        final List<Support> supports = getSupports(inter);
        final double cg = supports.isEmpty() ? inter.getGrade()
                : computeContextualGrade(inter, supports);
        inter.setContextualGrade(cg);

        return cg;
    }

    //-----------------//
    // containedInters //
    //-----------------//
    /**
     * Lookup the sig collection of interpretations for those which are contained in the
     * provided rectangle.
     *
     * @param rect the containing rectangle
     * @return the contained interpretations
     */
    public List<Inter> containedInters (Rectangle rect)
    {
        List<Inter> found = new ArrayList<>();

        for (Inter inter : vertexSet()) {
            final Rectangle box = inter.getBounds();

            if (box == null) {
                logger.error("No bounds for {}", inter);
            } else if (rect.contains(box)) {
                found.add(inter);
            }
        }

        return found;
    }

    //------------------//
    // containingInters //
    //------------------//
    /**
     * Lookup the sig collection of interpretations for those which contain the provided
     * point.
     *
     * @param point provided point
     * @return the containing interpretations
     */
    public List<Inter> containingInters (Point point)
    {
        List<Inter> found = new ArrayList<>();

        for (Inter inter : vertexSet()) {
            Rectangle bounds = inter.getBounds();

            if ((bounds != null) && bounds.contains(point)) {
                // More precise test if we know inter area
                Area area = inter.getArea();

                if ((area == null) || area.contains(point)) {
                    found.add(inter);
                }
            }
        }

        return found;
    }

    //---------------//
    // contextualize //
    //---------------//
    /**
     * (Re)compute the contextual grade of all inters based on their supporting partners.
     */
    public void contextualize ()
    {
        for (Inter inter : vertexSet()) {
            computeContextualGrade(inter);
        }
    }

    //--------------//
    // deleteInters //
    //--------------//
    /**
     * Remove a collection of inters.
     *
     * @param inters to delete
     */
    public void deleteInters (Collection<? extends Inter> inters)
    {
        for (Inter inter : inters) {
            inter.remove();
        }
    }

    //------------------//
    // deleteWeakInters //
    //------------------//
    /**
     * Purge the inter instances for which the contextual grade is lower than minimum
     * threshold.
     *
     * @return the set of inter instances purged
     */
    public Set<Inter> deleteWeakInters ()
    {
        Set<Inter> removed = new LinkedHashSet<>();

        for (Inter inter : vertexSet()) {
            // Skip frozen inters
            if (inter.isFrozen()) {
                continue;
            }

            // Ledgers are not concerned here, they will get deleted when no head is left
            if (inter.getShape() == Shape.LEDGER) {
                continue;
            }

            if (inter.getContextualGrade() < Grades.minContextualGrade) {
                if (inter.isVip()) {
                    logger.info("VIP deleted weak {}", inter);
                }

                removed.add(inter);
            }
        }

        deleteInters(removed);

        return removed;
    }

    //------------//
    // exclusions //
    //------------//
    /**
     * Report the set of exclusion relations currently present in SIG
     *
     * @return the set of exclusions
     */
    public Set<Relation> exclusions ()
    {
        Set<Relation> exclusions = new LinkedHashSet<>();

        for (Relation rel : edgeSet()) {
            if (rel instanceof Exclusion) {
                exclusions.add(rel);
            }
        }

        return exclusions;
    }

    /**
     * Across provided relation classes, build the closure of inter seeds.
     *
     * @param seeds           collection of inter seeds
     * @param relationClasses array of relation classes
     * @return the closure set
     */
    public Set<Inter> getClosureOf (final List<? extends Inter> seeds,
                                    final Class... relationClasses)
    {
        final Set<Inter> closure = new LinkedHashSet<>();

        class ClosureBuilder
        {

            public void browse (Inter seed)
            {
                for (Relation r : getRelations(seed, relationClasses)) {
                    Inter other = getOppositeInter(seed, r);

                    if (!closure.contains(other)) {
                        closure.add(other);
                        browse(other);
                    }
                }
            }
        }

        final ClosureBuilder builder = new ClosureBuilder();

        for (Inter seed : seeds) {
            if (!closure.contains(seed)) {
                builder.browse(seed);
            }
        }

        return closure;
    }

    //--------------//
    // getExclusion //
    //--------------//
    /**
     * Report the (first found) exclusion if any between the two provided inters.
     *
     * @param i1 an inter
     * @param i2 another inter
     * @return the (first) exclusion if any
     */
    public Exclusion getExclusion (Inter i1,
                                   Inter i2)
    {
        Set<Relation> exclusions = getExclusions(i1);
        exclusions.retainAll(getExclusions(i2));

        if (exclusions.isEmpty()) {
            return null;
        } else {
            return (Exclusion) exclusions.iterator().next();
        }
    }

    //---------------//
    // getExclusions //
    //---------------//
    /**
     * Report the set of conflicting relations the provided inter is involved in.
     *
     * @param inter the provided interpretation
     * @return the set of exclusions that involve inter, perhaps empty but not null
     */
    public Set<Relation> getExclusions (Inter inter)
    {
        return getRelations(inter, Exclusion.class);
    }

    //------------------//
    // getOppositeInter //
    //------------------//
    /**
     * Report the opposite inter across the given relation of the provided inter
     *
     * @param inter    one side of the relation
     * @param relation the relation to cross
     * @return the vertex at the opposite side of the relation
     */
    public Inter getOppositeInter (Inter inter,
                                   Relation relation)
    {
        return Graphs.getOppositeVertex(this, relation, inter);
    }

    //---------------//
    // getPartitions //
    //---------------//
    /**
     * Report all largest partitions of non-conflicting inters within the provided
     * collection of interpretations.
     *
     * @param focus  the inter instance, if any, for which partners are looked up
     * @param inters the provided collection of interpretations, with perhaps some mutual exclusion
     *               relations.
     * @return all the possible consistent partitions, with no pair of conflicting interpretations
     *         in the same partition
     */
    public List<List<Inter>> getPartitions (Inter focus,
                                            List<Inter> inters)
    {
        Collections.sort(inters, Inters.byReverseGrade);

        final int n = inters.size();
        final List<Inter> stems = (focus instanceof HeadInter) ? stemsOf(inters) : null;
        final List<List<Inter>> result = new ArrayList<>();

        // Map inter -> concurrents of inter (that appear later within the provided list)
        List<Set<Integer>> concurrentSets = new ArrayList<>();
        boolean conflictDetected = false;

        for (int i = 0; i < n; i++) {
            Inter inter = inters.get(i);
            Set<Integer> concurrents = new LinkedHashSet<>();
            concurrentSets.add(concurrents);

            for (Relation rel : getExclusions(inter)) {
                Inter concurrent = getOppositeInter(inter, rel);

                // Check whether this concurrent belongs to (and appears later in) the inters list
                int ic = inters.indexOf(concurrent);

                if (ic > i) {
                    concurrents.add(ic);
                    conflictDetected = true;
                }
            }

            //TODO: this is a hack that should be removed when
            // multiple stems for a head are correctly filtered out.
            // We assume that the various stems are potential partners of the focused head
            // and thus all stems are concurrent of one another
            if (focus instanceof HeadInter && inter instanceof StemInter) {
                // Flag all other stems, if any, as concurrents of this one
                for (Inter stem : stems) {
                    int ic = inters.indexOf(stem);

                    if (ic > i) {
                        concurrents.add(ic);
                        conflictDetected = true;
                    }
                }
            }
        }

        // If no conflict was detected, the provided collection is a single partition
        if (!conflictDetected) {
            result.add(inters);

            return result;
        }

        // Define all possible sequences
        List<Sequence> seqs = new ArrayList<>();
        seqs.add(new Sequence(n));

        for (int i = 0; i < n; i++) {
            Set<Integer> concurrents = concurrentSets.get(i);

            for (int is = 0, isBreak = seqs.size(); is < isBreak; is++) {
                Sequence seq = seqs.get(is);

                if (seq.line[i] != -1) {
                    seq.line[i] = 1;

                    if (!concurrents.isEmpty()) {
                        // Duplicate line
                        Sequence newSeq = seq.copy();
                        newSeq.line[i] = 0;
                        seqs.add(newSeq);

                        // Forbid dependent locations
                        for (Integer ic : concurrents) {
                            seq.line[ic] = -1;
                        }
                    }
                }
            }
        }

        // Build resulting partitions
        for (Sequence seq : seqs) {
            List<Inter> list = new ArrayList<>();

            for (int i = 0; i < n; i++) {
                if (seq.line[i] == 1) {
                    list.add(inters.get(i));
                }
            }

            result.add(list);
        }

        return result;
    }

    //-------------//
    // getRelation //
    //-------------//
    /**
     * Report the first relation if any of desired class between the provided source and
     * target vertices.
     *
     * @param source provided source
     * @param target provided target
     * @param classe desired class of relation
     * @return the existing relation if any, or null
     */
    public Relation getRelation (Inter source,
                                 Inter target,
                                 Class classe)
    {
        for (Relation rel : getAllEdges(source, target)) {
            if (classe.isInstance(rel)) {
                return rel;
            }
        }

        return null;
    }

    //--------------//
    // getRelations //
    //--------------//
    /**
     * Report the set of relations of desired classes the provided inter is involved in.
     *
     * @param inter   the provided interpretation
     * @param classes the desired classes of relation
     * @return the set of involving relations, perhaps empty but not null
     */
    public Set<Relation> getRelations (Inter inter,
                                       Class... classes)
    {
        Set<Relation> relations = new LinkedHashSet<>();

        for (Relation rel : edgesOf(inter)) {
            for (Class classe : classes) {
                if (classe.isInstance(rel)) {
                    relations.add(rel);
                }
            }
        }

        return relations;
    }

    //--------------//
    // getRelations //
    //--------------//
    /**
     * Report the set of relations of desired class the provided inter is involved in.
     *
     * @param inter  the provided interpretation
     * @param classe the desired class of relation
     * @return the set of involving relations, perhaps empty but not null
     */
    public Set<Relation> getRelations (Inter inter,
                                       Class classe)
    {
        Set<Relation> relations = new LinkedHashSet<>();

        for (Relation rel : edgesOf(inter)) {
            if (classe.isInstance(rel)) {
                relations.add(rel);
            }
        }

        return relations;
    }

    //-------------//
    // getSupports //
    //-------------//
    /**
     * Report the set of supporting relations the provided inter is involved in.
     *
     * @param inter the provided interpretation
     * @return set of supporting relations for inter, maybe empty but not null
     */
    public List<Support> getSupports (Inter inter)
    {
        List<Support> supports = new ArrayList<>();

        for (Relation rel : edgesOf(inter)) {
            if (rel instanceof Support) {
                supports.add((Support) rel);
            }
        }

        return supports;
    }

    //-----------//
    // getSystem //
    //-----------//
    /**
     * @return the related system
     */
    public SystemInfo getSystem ()
    {
        return system;
    }

    //-------------//
    // hasRelation //
    //-------------//
    /**
     * Check whether the provided Inter is involved in a relation of one of the
     * provided relation classes.
     *
     * @param inter           the inter instance to check
     * @param relationClasses the provided classes
     * @return true if such relation is found, false otherwise
     */
    public boolean hasRelation (Inter inter,
                                Class... relationClasses)
    {
        for (Relation rel : edgesOf(inter)) {
            for (Class classe : relationClasses) {
                if (classe.isInstance(rel)) {
                    return true;
                }
            }
        }

        return false;
    }

    //-------------------//
    // populateAllInters //
    //-------------------//
    /**
     * Minimal vertex addition, meant for just SIG bulk populating
     *
     * @param inters the inters to add to sig
     */
    public final void populateAllInters (Collection<? extends Inter> inters)
    {
        for (Inter inter : inters) {
            super.addVertex(inter);
        }
    }

    @Override
    public Object clone ()
    {
        return super.clone(); //To change body of generated methods, choose Tools | Templates.
    }

    //--------------//
    // getRelations //
    //--------------//
    /**
     * Report the set of relations of desired class out of the provided relations
     *
     * @param relations the provided relation collection
     * @param classe    the desired class of relation
     * @return the set of filtered relations, perhaps empty but not null
     */
    public static Set<Relation> getRelations (Collection<? extends Relation> relations,
                                              Class classe)
    {
        Set<Relation> found = new LinkedHashSet<>();

        for (Relation rel : relations) {
            if (classe.isInstance(rel)) {
                found.add(rel);
            }
        }

        return found;
    }

    //-----------------//
    // insertExclusion //
    //-----------------//
    /**
     * Insert an exclusion relation between two provided inters, unless there is a
     * support relation between them or unless an exclusion already exists.
     * <p>
     * Nota: We always insert exclusion from lower id to higher id.
     *
     * @param inter1 provided inter #1
     * @param inter2 provided inter #2
     * @param cause  exclusion cause (for creation only)
     * @return the concrete exclusion relation, found or created
     */
    public Exclusion insertExclusion (Inter inter1,
                                      Inter inter2,
                                      Cause cause)
    {
        final boolean direct = inter1.getId() < inter2.getId();
        final Inter source = direct ? inter1 : inter2;
        final Inter target = direct ? inter2 : inter1;

        {
            // Look for existing exclusion
            Relation rel = getRelation(source, target, Exclusion.class);

            if (rel != null) {
                return (Exclusion) rel;
            }
        }

        // Check no support relation exists, in either direction
        if (getRelation(source, target, Support.class) != null) {
            return null;
        }

        if (getRelation(target, source, Support.class) != null) {
            return null;
        }

        // Do insert an exclusion
        Exclusion exc = new Exclusion(cause);
        addEdge(source, target, exc);

        if (inter1.isVip() && inter2.isVip()) {
            logger.info("VIP exclusion {}", exc.toLongString(this));
        }

        return exc;
    }

    //------------------//
    // insertExclusions //
    //------------------//
    /**
     * Formalize mutual exclusion within a collection of inters
     *
     * @param inters the set of inters to mutually exclude
     * @param cause  the exclusion cause
     * @return the exclusions inserted
     */
    public List<Relation> insertExclusions (Collection<? extends Inter> inters,
                                            Cause cause)
    {
        List<Inter> list = new ArrayList<Inter>(new LinkedHashSet<>(inters));
        List<Relation> exclusions = new ArrayList<>();

        for (int i = 0, iBreak = list.size(); i < iBreak; i++) {
            Inter inter = list.get(i);

            for (Inter other : list.subList(i + 1, inters.size())) {
                exclusions.add(insertExclusion(inter, other, cause));
            }
        }

        return exclusions;
    }

    //---------------//
    // insertSupport //
    //---------------//
    /**
     * Insert a support between two provided inters, unless an exclusion exists or
     * unless such relation already exists between them.
     * <p>
     * Nota: We always insert such support from lower id to higher id.
     *
     * @param inter1       provided inter #1
     * @param inter2       provided inter #2
     * @param supportClass precise support to insert
     * @return the concrete support relation, found or created
     */
    public Support insertSupport (Inter inter1,
                                  Inter inter2,
                                  Class<? extends Support> supportClass)
    {
        final boolean direct = inter1.getId() < inter2.getId();
        final Inter source = direct ? inter1 : inter2;
        final Inter target = direct ? inter2 : inter1;

        // Look for existing exclusion
        Relation exc = getRelation(source, target, Exclusion.class);

        if (exc != null) {
            logger.debug("No support possible between exclusive {} & {}", source, target);

            return null;
        }

        // Look for existing support
        Relation rel = getRelation(source, target, supportClass);

        if (rel != null) {
            return (Support) rel;
        }

        // Do insert a support
        Support sup = null;

        try {
            sup = supportClass.newInstance();
            addEdge(source, target, sup);

            if (inter1.isVip() || inter2.isVip()) {
                logger.info("VIP support {}", sup.toLongString(this));
            }
        } catch (IllegalAccessException |
                 InstantiationException ex) {
            logger.error("Could not instantiate {} {}", supportClass, ex.toString(), ex);
        }

        return sup;
    }

    //--------//
    // inters //
    //--------//
    /**
     * Lookup for interpretations of the provided collection of shapes.
     *
     * @param shapes the shapes to check for
     * @return the interpretations of desired shapes, perhaps empty but not null
     */
    public List<Inter> inters (final Collection<Shape> shapes)
    {
        return inters(new ShapesPredicate(shapes));
    }

    //--------//
    // inters //
    //--------//
    /**
     * Select the inters that relate to the specified staff.
     *
     * @param staff the specified staff
     * @return the list of selected inters, perhaps empty but not null
     */
    public List<Inter> inters (Staff staff)
    {
        return Inters.inters(staff, vertexSet());
    }

    //--------//
    // inters //
    //--------//
    /**
     * Lookup for interpretations for which the provided predicate applies.
     *
     * @param predicate the predicate to apply, or null
     * @return the list of compliant interpretations, perhaps empty but not null
     */
    public List<Inter> inters (Predicate<Inter> predicate)
    {
        return Inters.inters(vertexSet(), predicate);
    }

    //--------//
    // inters //
    //--------//
    /**
     * Lookup for interpretations of the provided class (or subclass thereof).
     *
     * @param classe the class to search for
     * @return the interpretations of desired class, perhaps empty but not null
     */
    public List<Inter> inters (final Class classe)
    {
        return inters(new ClassPredicate(classe));
    }

    //--------//
    // inters //
    //--------//
    /**
     * Lookup for interpretations of the provided shape.
     *
     * @param shape the shape to check for
     * @return the interpretations of desired shape, perhaps empty but not null
     */
    public List<Inter> inters (final Shape shape)
    {
        return inters(new ShapePredicate(shape));
    }

    //--------//
    // inters //
    //--------//
    /**
     * Lookup for interpretations of the provided classes.
     *
     * @param classes array of desired classes
     * @return the interpretations of desired classes, perhaps empty but not null
     */
    public List<Inter> inters (final Class[] classes)
    {
        return inters(new ClassesPredicate(classes));
    }

    //--------//
    // inters //
    //--------//
    /**
     * Lookup for interpretations of the provided class, attached to the specified staff.
     *
     * @param staff  the specified staff
     * @param classe the class to search for
     * @return the list of interpretations found, perhaps empty but not null
     */
    public List<Inter> inters (final Staff staff,
                               final Class classe)
    {
        return inters(new StaffClassPredicate(staff, classe));
    }

    //-------------------//
    // intersectedInters //
    //-------------------//
    /**
     * Lookup all SIG inters for those whose bounds intersect the given box.
     *
     * @param box the intersecting box
     * @return the intersected interpretations found, perhaps empty but not null
     */
    public List<Inter> intersectedInters (Rectangle box)
    {
        List<Inter> found = new ArrayList<>();

        for (Inter inter : vertexSet()) {
            if (inter.isRemoved()) {
                continue;
            }

            if (box.intersects(inter.getBounds())) {
                found.add(inter);
            }
        }

        return found;
    }

    //-----------//
    // noSupport //
    //-----------//
    /**
     * Check for no existing support relation between the provided inters, regardless
     * of their order.
     *
     * @param one an inter
     * @param two another inter
     * @return true if no support exists between them, in either direction
     */
    public boolean noSupport (Inter one,
                              Inter two)
    {
        Set<Relation> rels = new LinkedHashSet<>();
        rels.addAll(getAllEdges(one, two));
        rels.addAll(getAllEdges(two, one));

        for (Relation rel : rels) {
            if (rel instanceof Support) {
                return false;
            }
        }

        return true;
    }

    //---------//
    // publish //
    //---------//
    /**
     * Convenient method to publish an Inter instance.
     *
     * @param inter the inter to publish (can be null)
     */
    public void publish (final Inter inter)
    {
        system.getSheet().getInterIndex().publish(inter);
    }

    //------------------//
    // reduceExclusions //
    //------------------//
    /**
     * Reduce the provided exclusions as much as possible by removing the source or
     * target vertex of lower contextual grade.
     * <p>
     * Strategy is as follows:
     * <ol>
     * <li>Pick up among all current exclusions the one whose high inter has the highest contextual
     * grade contribution among all exclusions,</li>
     * <li>Remove the weaker inter in this chosen exclusion relation,</li>
     * <li>Recompute all impacted contextual grades values,</li>
     * <li>Iterate until no more exclusion is left.</li>
     * </ol>
     *
     * @param exclusions the collection of exclusions to process
     * @return the set of vertices removed
     */
    public Set<Inter> reduceExclusions (Collection<? extends Relation> exclusions)
    {
        final Set<Inter> removed = new LinkedHashSet<>();
        Relation bestRel;

        do {
            // Choose exclusion with the highest source or target grade
            double bestCP = 0;
            bestRel = null;

            for (Iterator<? extends Relation> it = exclusions.iterator(); it.hasNext();) {
                Relation rel = it.next();

                if (containsEdge(rel)) {
                    final double cp = Math.max(
                            getEdgeSource(rel).getBestGrade(),
                            getEdgeTarget(rel).getBestGrade());

                    if (bestCP < cp) {
                        bestCP = cp;
                        bestRel = rel;
                    }
                } else {
                    it.remove();
                }
            }

            // Remove the weaker branch of the selected exclusion
            if (bestRel != null) {
                final Inter source = getEdgeSource(bestRel);
                final double scp = source.getBestGrade();
                final Inter target = getEdgeTarget(bestRel);
                final double tcp = target.getBestGrade();
                final Inter weaker = (scp < tcp) ? source : target;

                if (weaker.isVip()) {
                    logger.info(
                            "VIP conflict {} deleting weaker {}",
                            bestRel.toLongString(this),
                            weaker);
                }

                // Which inters were involved in some support relation with this weaker inter?
                final Set<Inter> involved = involvedInters(getSupports(weaker));
                involved.remove(weaker);

                final Set<Inter> weakerEnsembles = weaker.getAllEnsembles(); // Before weaker is deleted!

                // Remove the weaker inter
                removed.add(weaker);
                weaker.remove();

                // If removal of weaker has resulted in removal of an ensemble, count this ensemble
                for (Inter ensemble : weakerEnsembles) {
                    if (ensemble.isRemoved()) {
                        removed.add(ensemble);
                    }
                }

                // Update contextual values for all inters that were involved with 'weaker'
                for (Inter inter : involved) {
                    computeContextualGrade(inter);
                }

                exclusions.remove(bestRel);
            }
        } while (bestRel != null);

        return removed;
    }

    //------------------//
    // reduceExclusions //
    //------------------//
    /**
     * Reduce each exclusion in the SIG.
     *
     * @return the set of reduced inters
     */
    public Set<Inter> reduceExclusions ()
    {
        return reduceExclusions(exclusions());
    }

    //--------------//
    // removeVertex //
    //--------------//
    @Override
    public boolean removeVertex (Inter inter)
    {
        if (!inter.isRemoved()) {
            logger.error("Do not use removeVertex() directly. Use inter.remove() instead.");
            throw new IllegalStateException("Do not use removeVertex() directly");
        }

        // Remove from inter index. TODO: is this a good idea?
        system.getSheet().getInterIndex().remove(inter);

        if (inter.isVip()) {
            logger.info("VIP removeVertex {}", inter);
        }

        return super.removeVertex(inter);
    }

    //--------------//
    // sortBySource //
    //--------------//
    /**
     * Sort the provided list of relations by decreasing contextual grade of the
     * relations sources.
     *
     * @param rels the relations to sort
     */
    public void sortBySource (List<Relation> rels)
    {
        Collections.sort(rels, new Comparator<Relation>()
                 {
                     @Override
                     public int compare (Relation r1,
                                         Relation r2)
                     {
                         Inter s1 = getEdgeSource(r1);
                         Inter s2 = getEdgeSource(r2);

                         return Double.compare(s2.getBestGrade(), s1.getBestGrade());
                     }
                 });
    }

    //----------//
    // toString //
    //----------//
    @Override
    public String toString ()
    {
        StringBuilder sb = new StringBuilder(getClass().getSimpleName());
        sb.append("{");
        sb.append("S#").append(system.getId());
        sb.append(" inters:").append(vertexSet().size());
        sb.append(" relations:").append(edgeSet().size());
        sb.append("}");

        return sb.toString();
    }

    //---------------//
    // upgradeInters //
    //---------------//
    /**
     * All just loaded inters are checked and potentially upgraded.
     */
    @Deprecated
    private void upgradeInters ()
    {
        boolean upgraded = false;

        for (Inter inter : this.vertexSet()) {
            AbstractInter abstractInter = (AbstractInter) inter;
            upgraded |= abstractInter.upgradeOldStuff();
        }

        if (upgraded) {
            system.getSheet().getStub().setUpgraded(true);
        }
    }

    //------------------------//
    // computeContextualGrade //
    //------------------------//
    /**
     * Compute the contextual probability for a interpretation which is supported by
     * a collection of relations with partners.
     * <p>
     * It is assumed that all these supporting relations involve the inter as either a target or a
     * source, otherwise a runtime exception is thrown.
     * <p>
     * There may be mutual exclusion between some partners. In this case, we identify all partitions
     * of compatible partners and report the best resulting contextual contribution among those
     * partitions.
     *
     * @param inter    the inter whose contextual grade is to be computed
     * @param supports all supporting relations inter is involved with, some may be in conflict
     * @return the computed contextual grade
     */
    private Double computeContextualGrade (Inter inter,
                                           Collection<? extends Support> supports)
    {
        /** Collection of partners. */
        final List<Inter> partners = new ArrayList<>();

        /** Map: partner -> contribution. */
        final Map<Inter, Double> partnerContrib = new HashMap<>();

        // Check inter involvement
        for (Support support : supports) {
            final Inter partner;
            final double ratio;

            if (inter == getEdgeTarget(support)) {
                ratio = support.getTargetRatio();
                partner = getEdgeSource(support);
            } else if (inter == getEdgeSource(support)) {
                ratio = support.getSourceRatio();
                partner = getEdgeTarget(support);
            } else {
                throw new RuntimeException("No common interpretation");
            }

            if (ratio > 1) {
                partners.add(partner);
                partnerContrib.put(partner, partner.getGrade() * (ratio - 1));
            }
        }

        // Check for mutual exclusion between partners
        final List<List<Inter>> seqs = getPartitions(inter, partners);
        double bestCg = 0;

        for (List<Inter> seq : seqs) {
            double contribution = 0;

            for (Inter partner : seq) {
                contribution += partnerContrib.get(partner);
            }

            bestCg = Math.max(bestCg, GradeUtil.contextual(inter.getGrade(), contribution));
        }

        return bestCg;
    }

    //----------------//
    // involvedInters //
    //----------------//
    private Set<Inter> involvedInters (Collection<? extends Relation> relations)
    {
        Set<Inter> inters = new LinkedHashSet<>();

        for (Relation rel : relations) {
            inters.add(getEdgeSource(rel));
            inters.add(getEdgeTarget(rel));
        }

        return inters;
    }

    //---------//
    // stemsOf //
    //---------//
    private List<Inter> stemsOf (List<Inter> inters)
    {
        List<Inter> stems = new ArrayList<>();

        for (Inter inter : inters) {
            if (inter instanceof StemInter) {
                stems.add(inter);
            }
        }

        return stems;
    }

    //------------------//
    // supportsSeenFrom //
    //------------------//
    private String supportsSeenFrom (Inter inter,
                                     Map<Inter, Support> map,
                                     List<Inter> partners)
    {
        StringBuilder sb = new StringBuilder();

        for (Inter partner : partners) {
            if (sb.length() == 0) {
                sb.append("[");
            } else {
                sb.append(", ");
            }

            Support support = map.get(partner);
            sb.append(support.seenFrom(inter));
        }

        sb.append("]");

        return sb.toString();
    }

    //----------//
    // Sequence //
    //----------//
    /**
     * This class lists a sequence of interpretations statuses.
     * <p>
     * Possible status values are:
     * <ul>
     * <li>-1: the related inter is forbidden (because of a conflict with an inter located before in
     * the sequence)</li>
     * <li>0: the related inter is not selected</li>
     * <li>1: the related inter is selected</li>
     * </ul>
     */
    private static class Sequence
    {

        // The sequence of interpretations statuses
        // This line is parallel to the list of inters considered
        int[] line;

        Sequence (int n)
        {
            line = new int[n];
            Arrays.fill(line, 0);
        }

        public Sequence copy ()
        {
            Sequence newSeq = new Sequence(line.length);
            System.arraycopy(line, 0, newSeq.line, 0, line.length);

            return newSeq;
        }
    }

    //----------------//
    // ShapePredicate //
    //----------------//
    private static class ShapePredicate
            implements Predicate<Inter>
    {

        private final Shape shape;

        ShapePredicate (Shape shape)
        {
            this.shape = shape;
        }

        @Override
        public boolean check (Inter inter)
        {
            return !inter.isRemoved() && (inter.getShape() == shape);
        }
    }

    //-----------------//
    // ShapesPredicate //
    //-----------------//
    private static class ShapesPredicate
            implements Predicate<Inter>
    {

        private final Collection<Shape> shapes;

        ShapesPredicate (Collection<Shape> shapes)
        {
            this.shapes = shapes;
        }

        @Override
        public boolean check (Inter inter)
        {
            return !inter.isRemoved() && shapes.contains(inter.getShape());
        }
    }

    //---------------------//
    // StaffClassPredicate //
    //---------------------//
    private static class StaffClassPredicate
            implements Predicate<Inter>
    {

        private final Staff staff;

        private final Class classe;

        StaffClassPredicate (Staff staff,
                             Class classe)
        {
            this.staff = staff;
            this.classe = classe;
        }

        @Override
        public boolean check (Inter inter)
        {
            return !inter.isRemoved() && (inter.getStaff() == staff)
                           && ((classe == null) || classe.isInstance(inter));
        }
    }
}