/*
Copyright 2012-2014 Samuel Gesche

This file is part of the Greek Reuse Toolkit.

The Greek Reuse Toolkit is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

The Greek Reuse Toolkit 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with the Greek Reuse Toolkit.  If not, see <http://www.gnu.org/licenses/>.
*/

package fr.cnrs.liris.drim.grt.modele;

import java.util.ArrayList;

/**
 * Représente une référence sous la forme d'un amas de coordonnées. 
 * Une Référence n'a de sens que dans le cadre d'un document, mais elle a une 
 * existence propre indépendamment d'un document.
 * 
 * Une Référence peut avoir de 0 à 2 bornes. Une référence à 0 bornes est définie 
 * comme une imbrication d'autres références. 
 * Une référence à 1 bornes est un point (ex : 'le chapitre 5').
 * Une référence à deux bornes est un segment (ex : 'les chapitres 5 à 7').
 * 
 * Les références peuvent être imbriquées par inclusion ou exclusion (pour 
 * obtenir des motifs complexes tels que 'lignes 1 à 5 sauf 4 et 9 à 10'). 
 * Dans ce cas, les coordonnées de toutes les références inclues et exclues 
 * doivent être dans le même système (@see OrdreStrict).
 * 
 * Une référence peut être précisée par une (unique) autre référence, qui ne sera 
 * a priori pas dans le même système ('paragraphe 3' peut être précisé par 
 * 'ligne 5'). Une précision n'est possible que pour une référence qui a une borne 
 * de début et pas de borne de fin ; exception : si la borne de fin est à Coordonnee.FIN,
 * une précision s'applique à la borne de début.
 * 
 * Exemples de fabrications de référence de diverses complexité
 * 
 *      // Actes 5.2
        Reference r = new Reference();
        r.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.LIVRES_NTG), "Act"));
        
        Reference r2 = new Reference();
        r2.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.CHAPITRES), "5"));
        
        Reference r3 = new Reference();
        r3.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.VERSETS), "2"));
        
        r2.addSousReference(Reference.RELATION_PRECISION, r3);
        r.addSousReference(Reference.RELATION_PRECISION, r2);
        
        System.out.println(r);
        
        
        // Actes 5.2-8
        r = new Reference();
        r.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.LIVRES_NTG), "Act"));
        
        r2 = new Reference();
        r2.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.CHAPITRES), "5"));
        
        r3 = new Reference();
        r3.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.VERSETS), "2"));
        r3.setBorne(Reference.COTE_FIN, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.VERSETS), "8"));
        
        r2.addSousReference(Reference.RELATION_PRECISION, r3);
        r.addSousReference(Reference.RELATION_PRECISION, r2);
        
        System.out.println(r);
        
        
        // Actes 5.2-8, 6.3-12
        r = new Reference();
        r.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.LIVRES_NTG), "Act"));
        
        r2 = new Reference();
        r2.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.CHAPITRES), "5"));
        
        r3 = new Reference();
        r3.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.VERSETS), "2"));
        r3.setBorne(Reference.COTE_FIN, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.VERSETS), "8"));
        
        Reference r4 = new Reference();
        r4.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.CHAPITRES), "6"));
        
        Reference r5 = new Reference();
        r5.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.VERSETS), "3"));
        r5.setBorne(Reference.COTE_FIN, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.VERSETS), "12"));
        
        r4.addSousReference(Reference.RELATION_PRECISION, r5);
        r2.addSousReference(Reference.RELATION_INCLUSION, r4);
        r2.addSousReference(Reference.RELATION_PRECISION, r3);
        r.addSousReference(Reference.RELATION_PRECISION, r2);
        
        System.out.println(r);
        
        
        // Actes 5.2-8.12
        r = new Reference();
        r.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.LIVRES_NTG), "Act"));
        
        r2 = new Reference();
        r2.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.CHAPITRES), "5"));
        
        r3 = new Reference();
        r3.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.VERSETS), "2"));
        r3.setBorne(Reference.COTE_FIN, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.VERSETS), "fin"));
        
        Reference r6 = new Reference();
        r6.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.CHAPITRES), "6"));
        r6.setBorne(Reference.COTE_FIN, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.CHAPITRES), "7"));
        
        r4 = new Reference();
        r4.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.CHAPITRES), "8"));
        
        r5 = new Reference();
        r5.setBorne(Reference.COTE_DEBUT, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.VERSETS), "1"));
        r5.setBorne(Reference.COTE_FIN, new Coordonnee(OrdreStrict.getInstance(OrdreStrict.VERSETS), "12"));
        
        r2.addSousReference(Reference.RELATION_INCLUSION, r6);
        r4.addSousReference(Reference.RELATION_PRECISION, r5);
        r2.addSousReference(Reference.RELATION_INCLUSION, r4);
        r2.addSousReference(Reference.RELATION_PRECISION, r3);
        r.addSousReference(Reference.RELATION_PRECISION, r2);
        
        System.out.println(r);
 * 
 * @author Sam
 */
public class Reference {
    public final static int RELATION_INCLUSION = 1;
    public final static int RELATION_PRECISION = 0;
    public final static int RELATION_EXCLUSION = -1;
    
    public final static int COTE_DEBUT = 0;
    public final static int COTE_FIN = 1;
    
    private Coordonnee debut;
    private Coordonnee fin;
    
    private ArrayList<Reference> inclusions;
    private ArrayList<Reference> exclusions;
    private Reference precision;
    
    /**
     * Crée une Référence. Telle quelle, elle n'a pas de borne.
     */
    public Reference() {
        debut = null;
        fin = null;
        
        inclusions = new ArrayList<>();
        exclusions = new ArrayList<>();
        precision = null;
    }
    
    /**
     * Définit une des bornes de cette référence. Si une autre borne est déjà 
     * définie, la borne proposée doit avoir une coordonnée dans le même système.
     * 
     * La coordonnée de début doit être définie avant celle de fin.
     * 
     * @param cote soit COTE_DEBUT, soit COTE_FIN
     * @param valeur une Coordonnée à prendre comme borne (null pour supprimer 
     * la borne, supprimer la borne de début supprime la borne de fin)
     */
    public void setBorne(int cote, Coordonnee valeur) {
        if(cote == COTE_DEBUT) {
            if(valeur == null) {
                debut = null;
                fin = null;
            } else if(fin != null) {
                if(!(valeur.getSysteme().equals(fin.getSysteme()))) {
                    throw new IllegalArgumentException("La coordonnée proposée n'est pas dans le bon système.");
                }
                if(valeur.getDistance(fin) > 0) {
                    throw new IllegalArgumentException("La coordonnée proposée comme début est supérieure à celle de fin.");
                }
            }
            debut = valeur;
        } else if(cote == COTE_FIN) {
            if(valeur == null) {
                fin = null;
            } else if(debut != null) {
                if(!(valeur.getSysteme().equals(debut.getSysteme()))) {
                    throw new IllegalArgumentException("La coordonnée proposée n'est pas dans le bon système.");
                }
                if(!valeur.getExpression().equals(Coordonnee.FIN) && valeur.getDistance(debut) > 0) {
                    //System.err.println(valeur.toString() + " < " + debut.toString());
                    throw new IllegalArgumentException("La coordonnée proposée comme fin est inférieure à celle de début.");
                }
            } else {
                throw new IllegalArgumentException("Inutile de définir une coordonnée de fin s'il n'y en a pas au début.");
            }
            fin = valeur;
        } else {
            throw new IllegalArgumentException("Le côté de la borne doit être "
                    + "soit COTE_DEBUT, soit COTE_FIN.");
        }
    }
    
    /**
     * Dit si oui ou non la référence a une borne du côté demandé.
     * 
     * Il s'agit des bornes elles-mêmes définies par setBorne(), pas du résultat 
     * d'un calcul, qui nécessite un document sur lequel plaquer la 
     * référence.
     * 
     * @param cote le côté dont on veut savoir s'il y a une borne 
     * (soit COTE_DEBUT, soit COTE_FIN)
     * @return oui ou non
     */
    public boolean hasBorne(int cote) {
        if(cote == COTE_DEBUT) {
            return debut != null;
        } else if(cote == COTE_FIN) {
            return fin != null;
        } else {
            throw new IllegalArgumentException("Le côté de la borne doit être "
                    + "soit COTE_DEBUT, soit COTE_FIN.");
        }
    }
    
    /**
     * Donne la coordonnée d'une borne de l'intervalle défini par cette référence.
     * 
     * Il s'agit des bornes elles-mêmes définies par setBorne(), pas du résultat 
     * d'un calcul, qui nécessite un document sur lequel plaquer la 
     * référence.
     * 
     * @param cote le côté dont on veut la borne (soit COTE_DEBUT, soit COTE_FIN)
     * @return la Coordonnée correspondante (null s'il n'y en a pas)
     */
    public Coordonnee getBorne(int cote) {
        if(cote == COTE_DEBUT) {
            return debut;
        } else if(cote == COTE_FIN) {
            return fin;
        } else {
            throw new IllegalArgumentException("Le côté de la borne doit être "
                    + "soit COTE_DEBUT, soit COTE_FIN.");
        }
    }
    
    public int getNbBornes() {
        int res = 0;
        if(debut != null) {
            res++;
        }
        if(fin != null) {
            res++;
        }
        return res;
    }
    
    public void addSousReference(int relation, Reference sousRef) {
        if(relation == RELATION_INCLUSION) {
            inclusions.add(sousRef);
        } else if(relation == RELATION_EXCLUSION) {
            exclusions.add(sousRef);
        } else if(relation == RELATION_PRECISION) {
            if(debut == null || fin != null) {
                if(!fin.getExpression().equals(Coordonnee.FIN)) {
                    throw new IllegalArgumentException("On ne peut pas préciser une "
                            + "référence ayant zéro ou deux bornes (sauf si la "
                            + "borne de fin est à Coordonnee.FIN.");
                }
            }
            precision = sousRef;
            if(fin != null) {
                if(fin.getExpression().equals(Coordonnee.FIN)) {
                    // La sous-référence doit avoir une borne de fin à Coordonnee.FIN également
                    precision.setBorne(COTE_FIN, new Coordonnee(precision.getBorne(COTE_DEBUT).getSysteme(), Coordonnee.FIN));
                }
            }
        } else {
            throw new IllegalArgumentException("La relation entre cette "
                    + "référence et celle proposée doit être soit "
                    + "RELATION_INCLUSION, soit RELATION_EXCLUSION, "
                    + "soit RELATION_PRECISION.");
        }
    }
    
    public Reference[] getSousReferences(int relation) {
        Reference[] resultat;
        if(relation == RELATION_INCLUSION) {
            resultat = new Reference[inclusions.size()];
            inclusions.toArray(resultat);
        } else if(relation == RELATION_EXCLUSION) {
            resultat = new Reference[exclusions.size()];
            exclusions.toArray(resultat);
        } else if(relation == RELATION_PRECISION) {
            if(precision != null) {
                resultat = new Reference[]{precision};
            } else {
                resultat = new Reference[]{};
            }
        } else {
            throw new IllegalArgumentException("La relation demandée doit être "
                    + "soit RELATION_INCLUSION, soit RELATION_EXCLUSION, "
                    + "soit RELATION_PRECISION.");
        }
        return resultat;
    }
    
    public boolean hasSousReference(int relation) {
        boolean resultat = false;
        if(relation == RELATION_INCLUSION) {
            resultat = inclusions.size()>0;
        } else if(relation == RELATION_EXCLUSION) {
            resultat = exclusions.size()>0;
        } else if(relation == RELATION_PRECISION) {
            resultat = precision != null;
        } else {
            throw new IllegalArgumentException("La relation demandée doit être "
                    + "soit RELATION_INCLUSION, soit RELATION_EXCLUSION, "
                    + "soit RELATION_PRECISION.");
        }
        return resultat;
    }
    
    public boolean hasSousReference() {
        return inclusions.size()>0 || exclusions.size()>0 || precision != null;
    }
    
    @Override
    public String toString() {
        String resultat = "";
        // affichage de la référence elle-même
        if(debut != null) {
            resultat += debut.getSysteme().toString() + " " + debut.getExpression();
            if(fin != null) {
                resultat += "-" + fin.getExpression();
            }
            if(precision != null) {
                resultat += " " + precision.toString();
            }
        } else {
            // rien à afficher ici
        }
        
        // affichage des références incluses
        if(!inclusions.isEmpty()) {
            for(Reference r: inclusions) {
                resultat += " ; " + r.toString();
            }
        }
        // affichage des références exclues
        if(!exclusions.isEmpty()) {
            for(Reference r: exclusions) {
                resultat += " sauf " + r.toString();
            }
        }
        
        return resultat;
    }
}
