/*
 *                    BioJava development code
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU Lesser General Public Licence.  This should
 * be distributed with the code.  If you do not have a copy,
 * see:
 *
 *      http://www.gnu.org/copyleft/lesser.html
 *
 * Copyright for this code is held jointly by the individual
 * authors.  These should be listed in @author doc comments.
 *
 * For more information on the BioJava project and its aims,
 * or to join the biojava-l mailing list, visit the home page
 * at:
 *
 *      http://www.biojava.org/
 *
 */

package org.biojavax.ontology;

import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.biojava.ontology.AlreadyExistsException;
import org.biojava.ontology.DefaultOps;
import org.biojava.ontology.Ontology;
import org.biojava.ontology.OntologyOps;
import org.biojava.ontology.Term;
import org.biojava.ontology.Triple;
import org.biojava.ontology.Variable;
import org.biojava.utils.AbstractChangeable;
import org.biojava.utils.ChangeEvent;
import org.biojava.utils.ChangeSupport;
import org.biojava.utils.ChangeVetoException;

/**
 * Represents an ontology that can be compared to other ontologies.
 * @author Richard Holland
 * @author Mark Schreiber
 * @since 1.5
 */
public class SimpleComparableOntology extends AbstractChangeable implements ComparableOntology {
    
    private String name;
    private String description;
    private Set terms = new TreeSet();
    private Map termsMap = new TreeMap();
    private Set triples = new TreeSet();
    private OntologyOps ops;
    
    /**
     * Creates a new instance of SimpleComparableOntology with the given,
     * immutable, non-nullable name.
     * @param name the name of the ontology.
     */
    public SimpleComparableOntology(String name) {
        if (name==null) throw new IllegalArgumentException("Name cannot be null");
        this.name = name;
        this.description = null;
        this.ops = new DefaultOps() {
            public Set getRemoteTerms() {
                return Collections.EMPTY_SET;
            }
        };
    }
    
    // Hibernate requirement - not for public use.
    protected SimpleComparableOntology() {}
    
    /**
     * {@inheritDoc}
     * Ontologies are compared only by name.
     */
    public int compareTo(Object o) {
        if (o==this) return 0;
        // Hibernate comparison - we haven't been populated yet
        if (this.name==null) return -1;
        // Normal comparison
        Ontology them = (Ontology)o;
        return this.name.compareTo(them.getName());
    }
    
    /**
     * {@inheritDoc}
     * Ontologies are equal if their names are equal.
     */
    public boolean equals(Object obj) {
        if(this == obj) return true;
        if (obj==null || !(obj instanceof Ontology)) return false;
        // Hibernate comparison - we haven't been populated yet
        if (this.name==null) return false;
        // Normal comparison
        Ontology them = (Ontology)obj;
        return this.name.equals(them.getName());
    }
    
    /**
     * {@inheritDoc}
     */
    public int hashCode() {
        int hash = 17;
        // Hibernate comparison - we haven't been populated yet
        if (this.name==null) return hash;
        // Normal comparison
        return 31*hash + this.name.hashCode();
    }
    
    /**
     * {@inheritDoc}
     * Form: "name"
     */
    public String toString() {
        return this.getName();
    }
    
    /**
     * {@inheritDoc}
     */
    public boolean containsTerm(String name) { return this.termsMap.containsKey(name); }
    
    /**
     * {@inheritDoc}
     */
    public Term getTerm(String s) throws NoSuchElementException {
        if (!this.termsMap.containsKey(s)) throw new NoSuchElementException("Ontology does not have term with name "+s);
        return (ComparableTerm)this.termsMap.get(s);
    }
    
    /**
     * {@inheritDoc}
     * If the term has to be created, it is added with the description "auto-generated by biojavax".
     */
    public ComparableTerm getOrCreateTerm(String name) {
        try {
            if (!this.termsMap.containsKey(name)) return (ComparableTerm)this.createTerm(name,"auto-generated by biojavax",null);
            else return (ComparableTerm)this.getTerm(name);
        } catch (ChangeVetoException e) {
            return null;
        } catch (AlreadyExistsException e) {
            return (ComparableTerm)this.getTerm(name);
        }
    }
    
    
    /**
     *{@inheritDoc}
     */
    public ComparableTriple getOrCreateTriple(Term subject, Term object, Term predicate){
        
        try {
            if (this.getTriples(subject, object, predicate).size() == 0) {
                return (ComparableTriple)this.createTriple(subject, object, predicate, 
                    predicate.getName()+"("+subject.getName()+", "+object.getName()+")",
                        null);
            }
            else return (ComparableTriple)this.getTriples(subject, object, predicate).iterator().next();
        } catch (ChangeVetoException e) {
            return null;
        } catch (AlreadyExistsException e) {
            return (ComparableTriple)this.getTriples(subject, object, predicate).iterator().next();
        }
    }
    /**
     * {@inheritDoc}
     */
    public ComparableTerm getOrImportTerm(Term term) {
        //if (term instanceof ComparableTerm) return (ComparableTerm)term;
        try {
            if (!this.termsMap.containsKey(term.getName())) return (ComparableTerm)this.importTerm(term,term.getName());
            else return (ComparableTerm)this.getTerm(term.getName());
        } catch (ChangeVetoException e) {
            return null;
        }
    }
    
    /**
     * {@inheritDoc}
     */
    public Term createTerm(String name, String description, Object[] synonyms) throws AlreadyExistsException, ChangeVetoException, IllegalArgumentException {
        if (name==null) throw new IllegalArgumentException("Name cannot be null");
        if (this.termsMap.containsKey(name)) throw new AlreadyExistsException("Ontology already has term with this name");
        ComparableTerm ct = new SimpleComparableTerm(this,name,synonyms);
        ct.setDescription(description);
        if(!this.hasListeners(ComparableOntology.TERM)) {
            this.termsMap.put(name,ct);
            this.terms.add(ct);
        } else {
            ChangeEvent ce = new ChangeEvent(
                    this,
                    ComparableOntology.TERM,
                    ct,
                    this.termsMap.get(name)
                    );
            ChangeSupport cs = this.getChangeSupport(ComparableOntology.TERM);
            synchronized(cs) {
                cs.firePreChangeEvent(ce);
                this.termsMap.put(name,ct);
                this.terms.add(ct);
                cs.firePostChangeEvent(ce);
            }
        }
        return ct;
    }
    
    /**
     * {@inheritDoc}
     * This particular implementation merely copies the term into this ontology, 
     * and returns a pointer to the copied term. Thus the term
     * becomes a part of this ontology instead of a pointer to another ontology.
     * @see ComparableTerm
     */
    public Term importTerm(Term t, String localName) throws ChangeVetoException, IllegalArgumentException {
        if (localName==null) localName=t.getName();
        if (localName==null) throw new IllegalArgumentException("Name cannot be null");
        if (this.termsMap.containsKey(localName)) return (ComparableTerm)this.termsMap.get(localName);
        ComparableTerm ct = new SimpleComparableTerm(this,localName,t.getSynonyms());
        ct.setDescription(t.getDescription());
        if(!this.hasListeners(ComparableOntology.TERM)) {
            this.termsMap.put(localName,ct);
            this.terms.add(ct);
        } else {
            ChangeEvent ce = new ChangeEvent(
                    this,
                    ComparableOntology.TERM,
                    ct,
                    this.termsMap.get(localName)
                    );
            ChangeSupport cs = this.getChangeSupport(ComparableOntology.TERM);
            synchronized(cs) {
                cs.firePreChangeEvent(ce);
                this.termsMap.put(localName,ct);
                this.terms.add(ct);
                cs.firePostChangeEvent(ce);
            }
        }
        return ct;
    }
    
    /**
     * {@inheritDoc}
     * If you call this method with plain Terms instead of ComparableTerms, it will 
     * import them into the local ontology first. This is done BEFORE the check to
     * see if the triple already exists, as obviously it can't check until it has
     * the right terms to check with. So you may find the terms get imported but the
     * triple does not. Moral of the story: use ComparableTerm objects!    
     * @see ComparableTerm
     */
    public Triple createTriple(Term subject, Term object, Term predicate, String name, String description) throws AlreadyExistsException, ChangeVetoException {
        if (!(subject instanceof ComparableTerm)) subject = this.getOrImportTerm(subject);
        if (!(object instanceof ComparableTerm)) object = this.getOrImportTerm(object);
        if (!(predicate instanceof ComparableTerm)) predicate = this.getOrImportTerm(predicate);
        if (this.containsTriple(subject,object,predicate)) 
            throw new AlreadyExistsException("Ontology already has triple with this subject/object/predicate combination");
        ComparableTriple ct = new SimpleComparableTriple(this,(ComparableTerm)subject,(ComparableTerm)object,(ComparableTerm)predicate);
        if (!this.triples.contains(ct)) {
            if(!this.hasListeners(ComparableOntology.TRIPLE)) {
                this.triples.add(ct);
            } else {
                ChangeEvent ce = new ChangeEvent(
                        this,
                        ComparableOntology.TRIPLE,
                        ct,
                        null
                        );
                ChangeSupport cs = this.getChangeSupport(ComparableOntology.TRIPLE);
                synchronized(cs) {
                    cs.firePreChangeEvent(ce);
                    this.triples.add(ct);
                    cs.firePostChangeEvent(ce);
                }
            }
        }
        return ct;
    }
    
    /**
     * {@inheritDoc}
     * If you call this method with a plain Term instead of a ComparableTerm, it may
     * not match any of the terms in the ontology as they are all stored as 
     * ComparableTerms. So, use ComparableTerm objects!
     * This method also removes all triples that the term is involved in.
     * @see ComparableTerm
     */
    public void deleteTerm(Term t) throws ChangeVetoException {
        // Remove all Triples involving this term.
        for (Iterator i = this.triples.iterator(); i.hasNext();) {
            ComparableTriple ct = (ComparableTriple)i.next();
            if (ct.equals(t) || ct.getSubject().equals(t) || ct.getObject().equals(t) || ct.getPredicate().equals(t)) {
                if(!this.hasListeners(ComparableOntology.TRIPLE)) {
                    i.remove();
                } else {
                    ChangeEvent ce = new ChangeEvent(
                            this,
                            ComparableOntology.TRIPLE,
                            null,
                            ct
                            );
                    ChangeSupport cs = this.getChangeSupport(ComparableOntology.TRIPLE);
                    synchronized(cs) {
                        cs.firePreChangeEvent(ce);
                        i.remove();
                        cs.firePostChangeEvent(ce);
                    }
                }
            }
        }
        // Remove the term.
        if(!this.hasListeners(ComparableOntology.TERM)) {
            if (t instanceof Triple) this.triples.remove(t);
            else {
                this.termsMap.remove(t.getName());
                this.terms.remove(t);
            }
        } else {
            ChangeEvent ce = new ChangeEvent(
                    this,
                    ComparableOntology.TERM,
                    null,
                    t
                    );
            ChangeSupport cs = this.getChangeSupport(ComparableOntology.TERM);
            synchronized(cs) {
                cs.firePreChangeEvent(ce);
                if (t instanceof Triple) this.triples.remove(t);
                else {
                    this.termsMap.remove(t.getName());
                    this.terms.remove(t);
                }
                cs.firePostChangeEvent(ce);
            }
        }
    }
    
    /**
     * {@inheritDoc}
     * If you call this method with plain Terms instead of ComparableTerms, it may
     * not match any of the triples in the ontology as they are all stored as 
     * ComparableTerms. So, use ComparableTerm objects! The set returned is a set
     * of ComparableTriple objects.
     * @see ComparableTerm
     * @see ComparableTriple
     */
    public Set getTriples(Term subject, Term object, Term predicate) {
        Set results = new TreeSet();
        for (Iterator i = this.triples.iterator(); i.hasNext();) {
            ComparableTriple ct = (ComparableTriple)i.next();
            if ((subject==null || ct.getSubject().equals(subject)) &&
                    (object==null || ct.getObject().equals(object)) &&
                    (predicate==null || ct.getPredicate().equals(predicate))) results.add(ct);
        }
        return results;
    }
    
    /**
     * {@inheritDoc}
     * <b>Warning</b> this method gives access to the original 
     * Collection not a copy. This is required by Hibernate. If you
     * modify the object directly the behaviour may be unpredictable.
     */
    public void setTripleSet(Set triples) throws ChangeVetoException { this.triples = triples; }  // must be original for Hibernate
    
    /**
     * {@inheritDoc}
     * <b>Warning</b> this method gives access to the original 
     * Collection not a copy. This is required by Hibernate. If you
     * modify the object directly the behaviour may be unpredictable.
     */
    public Set getTripleSet() { return this.triples; } // must be original for Hibernate
    
    /**
     * {@inheritDoc}
     * This will always return a set of ComparableTerm objects. It is not the original
     * set so you are safe to modify it.
     * @see ComparableTerm
     */
    public Set getTerms() { return new TreeSet(this.termsMap.values()); }
    
    /**
     * {@inheritDoc}
     * <b>Warning</b> this method gives access to the original 
     * Collection not a copy. This is required by Hibernate. If you
     * modify the object directly the behaviour may be unpredictable.
     * @see ComparableTerm
     */
    public void setTermSet(Set terms) throws ChangeVetoException {
        this.terms = terms; // must be original for Hibernate
        this.termsMap.clear();
        for (Iterator i = this.terms.iterator(); i.hasNext(); ) {
            ComparableTerm t = (ComparableTerm)i.next();
            this.termsMap.put(t.getName(),t);
        }
    }
    
    /**
     * {@inheritDoc}
     * <b>Warning</b> this method gives access to the original 
     * Collection not a copy. This is required by Hibernate. If you
     * modify the object directly the behaviour may be unpredictable.
     */
    public Set getTermSet() { return this.terms; } // must be original for Hibernate
    
    /**
     * {@inheritDoc}
     * If you call this method with plain Terms instead of ComparableTerms, it may
     * not match any of the triples in the ontology as they are all stored as 
     * ComparableTerms. So, use ComparableTerm objects! The set returned is a set
     * of ComparableTriple objects.
     * @see ComparableTerm
     * @see ComparableTriple
     */
    public boolean containsTriple(Term subject, Term object, Term predicate) {
        for (Iterator i = this.triples.iterator(); i.hasNext();) {
            ComparableTriple ct = (ComparableTriple)i.next();
            if (ct.getSubject().equals(subject) &&
                    ct.getObject().equals(object) &&
                    ct.getPredicate().equals(predicate)) return true;
        }
        return false;
    }
    
    /**
     * {@inheritDoc}
     */
    public Term createTerm(String name) throws AlreadyExistsException, ChangeVetoException, IllegalArgumentException {
        return this.createTerm(name,null,null);
    }
    
    /**
     * {@inheritDoc}
     */
    public Term createTerm(String name, String description) throws AlreadyExistsException, ChangeVetoException, IllegalArgumentException {
        return this.createTerm(name,description,null);
    }
    
    /**
     * {@inheritDoc}
     * NOT IMPLEMENTED
     */
    public Variable createVariable(String name, String description) throws AlreadyExistsException, ChangeVetoException, IllegalArgumentException {
        throw new ChangeVetoException("BioSQL doesn't know what these are so we cowardly refuse to know too.");
    }
    
    /**
     * {@inheritDoc}
     */
    public String getDescription() {  return this.description; }
    
    /**
     * {@inheritDoc}
     */
    public void setDescription(String description) throws ChangeVetoException {
        if(!this.hasListeners(ComparableOntology.DESCRIPTION)) {
            this.description = description;
        } else {
            ChangeEvent ce = new ChangeEvent(
                    this,
                    ComparableOntology.DESCRIPTION,
                    description,
                    this.description
                    );
            ChangeSupport cs = this.getChangeSupport(ComparableOntology.DESCRIPTION);
            synchronized(cs) {
                cs.firePreChangeEvent(ce);
                this.description = description;
                cs.firePostChangeEvent(ce);
            }
        }
    }
    
    /**
     * {@inheritDoc}
     */
    public String getName() { return this.name; }
    
    // Hibernate requirement - not for public use.
    public void setName(String name) { this.name = name; }
    
    /**
     * {@inheritDoc}
     */
    public OntologyOps getOps() { return this.ops; }
    
    // Hibernate requirement - not for public use.
    private Integer id;
    
    /**
     * Gets the Hibernate ID. Should be used with caution.
     * @return the Hibernate ID, if using Hibernate.
     */
    public Integer getId() { return this.id; }
    
    /**
     * Sets the Hibernate ID. Should be used with caution.
     * @param id the Hibernate ID, if using Hibernate.
     */
    public void setId(Integer id) { this.id = id;}
    
}

