/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

package com.arsdigita.london.terms;

import com.arsdigita.kernel.ACSObject;
import com.arsdigita.domain.DomainObjectFactory;
import com.arsdigita.domain.DeleteCheckObserver;
import com.arsdigita.domain.DomainCollection;
import com.arsdigita.domain.DataObjectNotFoundException;
import com.arsdigita.domain.ObservableDomainObject;
import com.arsdigita.categorization.Category;
import com.arsdigita.persistence.DataObject;
import com.arsdigita.persistence.DataCollection;
import com.arsdigita.persistence.DataQuery;
import com.arsdigita.persistence.DataQueryDataCollectionAdapter;
import com.arsdigita.persistence.SessionManager;
import com.arsdigita.util.Assert;
import com.arsdigita.util.UncheckedWrapperException;

import java.util.Date;

import java.net.URL;
import java.net.MalformedURLException;

import org.apache.log4j.Logger;


/**
 * A domain is an abstract space containing a set
 * of terms.
 */
public class Domain extends ObservableDomainObject {
    
    private static final Logger s_log = Logger.getLogger(Domain.class);

    public static final String BASE_DATA_OBJECT_TYPE = 
        "com.arsdigita.london.terms.Domain";

    public static final String KEY = "key";
    public static final String URL = "url";
    public static final String TITLE = "title";
    public static final String DESCRIPTION = "description";
    public static final String VERSION = "version";
    public static final String RELEASED = "released";
    public static final String TERMS = "terms";
    public static final String MODEL = "model";

    Domain() {
        this(BASE_DATA_OBJECT_TYPE);
    }
    
    protected Domain(String type) {
        super(type);
    }
    
    Domain(DataObject dobj) {
        super(dobj);
    }

    public void initialize() {
        super.initialize();
        
        if (false && isNew()) {
            Category model = new Category();
            model.setAbstract(true);
            set(MODEL, model);
        }
    }
    
    /**
     * Creates a new domain object. All parameters are
     * required, except for description.
     *
     * @param key the unique key identifying the domain
     * @param url the unique URL defining the domain
     * @param title the short name of the domain
     * @param description the optional long description
     * @param version the version string for the domain
     * @param released the date on which the version was released
     * @return the newly created domain
     */
    public static Domain create(String key,
                                URL url,
                                String title,
                                String description,
                                String version,
                                Date released) {
        Domain domain = new Domain();
        domain.setKey(key);

        Category model = new Category();
        model.setAbstract(true);
        domain.set(MODEL, model);

        domain.setURL(url);
        domain.setTitle(title);
        domain.setDescription(description);
        domain.setVersion(version);
        domain.setReleased(released);

        return domain;
    }
    
    /**
     * Retrieve a domain based on its unique key
     * @param key the unique key of the domain
     * @return the domain corresponding to the key
     * @throws DataObjectNotFoundException if no matching domain is found
     */
    public static Domain retrieve(String key) {
        DataCollection domains = SessionManager.getSession()
            .retrieve(BASE_DATA_OBJECT_TYPE);
        
        domains.addEqualsFilter(KEY, key);
        
        if (domains.next()) {
            Domain domain = (Domain)
                DomainObjectFactory.newInstance(domains.getDataObject());
            domains.close();
            return domain;
        }
        throw new DataObjectNotFoundException(
            "Domain with key " + key + " not found"
        );
    }

    /**
     * Finds a domain based on its url
     * @param url the location of the domain
     * @return the domain corresponding to the url
     * @throws DataObjectNotFoundException if no matching domain is found
     */
    public static Domain find(URL url) {
        DataCollection domains = SessionManager.getSession()
            .retrieve(BASE_DATA_OBJECT_TYPE);
        
        domains.addEqualsFilter(URL, url.toString());
        
        if (domains.next()) {
            Domain domain = (Domain)
                DomainObjectFactory.newInstance(domains.getDataObject());
            domains.close();
            return domain;
        }
        throw new DataObjectNotFoundException(
            "Domain with url " + url + " not found"
        );
    }
    
    
    /**
     * Retrieves the collection of all terms in this
     * domain.
     */
    public DomainCollection getTerms() {
        DataCollection terms = (DataCollection)get(TERMS);
        return new DomainCollection(terms);
    }

    /**
     * Retrieves the term within this domain with
     * the corresponding unique identifier.
     *
     * @param uniqueID the id of the term to retrieve
     * @return the term matching the unique id.
     */
    public Term getTerm(Integer uniqueID) {
        DomainCollection terms = getTerms();
        terms.addEqualsFilter(Term.UNIQUE_ID, uniqueID);
        if (terms.next()) {
            Term term = (Term)terms.getDomainObject();
            terms.close();
            return term;
        } 
        throw new DataObjectNotFoundException(
            "Term " + uniqueID + " not found in domain " + getKey()
        );
    }
    
    /**
     * Retrieves the collection of terms in this domain
     * which are associated with the object.
     * @param obj the object to get terms for
     * @return the collection of terms for this object
     */
    public DomainCollection getDirectTerms(ACSObject obj) {
        DomainCollection terms = getTerms();
        terms.addEqualsFilter("model.childObjects.id", obj.getID());
        return terms;
    }
    
    /*
     * Generates the collection of terms in another domain
     * by matching against terms in this domain.
     *
     * @param obj the object to get terms for
     * @param domain the domain to get terms for
     * @return the collection of terms for this object
     */
    /*
    public DomainCollection getImpliedTerms(ACSObject obj,
                                            Domain domain) {
        // XXX implement me
    }
    */

    public Category getModel() {
        return (Category)DomainObjectFactory
            .newInstance((DataObject)get(MODEL));
    }

    
    private void setKey(String key) {
        Assert.exists(key, String.class);
        set(KEY, key);
    }

    /**
     * Retrieves the unique key for this domain
     * @return this domain's unique key
     */
    public String getKey() {
        return (String)get(KEY);
    }
    
    /**
     * Updates the unique URL defining this domain
     * @param url the new unique url for this domain
     */
    public void setURL(URL url) {
        Assert.exists(url, URL.class);
        set(URL, url.toString());
    }
    
    /**
     * Retrieves the unique URL defining this domain
     * @return this domain's unique URL
     */
    public URL getURL() {
        String url = (String)get(URL);
        try {
            return new URL(url);
        } catch (MalformedURLException ex) {
            throw new UncheckedWrapperException("Cannot parse url " + url, ex);
        }
    }
    
    /**
     * Updates the short title for this domain
     * @param title the new title
     */
    public void setTitle(String title) {
        Assert.exists(title, String.class);
        set(TITLE, title);
        getModel().setName(title);
    }
    
    /**
     * Retrieves the short title for this domain
     * @return this domain's title
     */
    public String getTitle() {
        return (String)get(TITLE);
    }

    /**
     * Updates the long description for this domain
     * @param description the domain's description
     */
    public void setDescription(String description) {
        set(DESCRIPTION, description);
        getModel().setDescription(description);
    }
    
    /** 
     * Retrieves the long description for this domain
     * @return the domain's description
     */
    public String getDescription() {
        return (String)get(DESCRIPTION);
    }
    
    /**
     * Updates the version key for this domain
     * @param the new version key
     */
    public void setVersion(String version) {
        Assert.exists(version, String.class);
        set(VERSION, version);
    }
    
    /**
     * Retrieves the version key for this domain
     * @return this domain's version key
     */
    public String getVersion() {
        return (String)get(VERSION);
    }

    /**
     * Updates the release date for this version
     * of the domain
     * @param released the new release date
     */
    public void setReleased(Date released) {
        Assert.exists(released, Date.class);
        set(RELEASED, released);
    }
    
    /**
     * Retrieves the release date for this version
     * of the domain
     * @return this domain's release date
     */
    public Date getReleased() {
        return (Date)get(RELEASED);
    }

    
    /**
     * Adds a root term to this domain
     * @param term the root term
     */
    public void addRootTerm(Term term) {
        Assert.truth(term.getDomain().equals(this),
                     "root term is in this domain");

        if (s_log.isDebugEnabled()) {
            s_log.debug("Adding root term " + term + " to " + 
                        this);
        }

        getModel().addChild(term.getModel());
        term.getModel().setDefaultParentCategory(getModel());
    }
    
    public void removeRootTerm(Term term) {
        Assert.truth(term.getDomain().equals(this),
                     "root term is in this domain");

        if (s_log.isDebugEnabled()) {
            s_log.debug("Removing root term " + term + " from " + 
                        this);
        }

        getModel().removeChild(term.getModel());
    }
    
    /**
     * Retrieves all the root terms
     * @return a collection of root terms
     */
    public DomainCollection getRootTerms() {
        DomainCollection terms = getTerms();
        terms.addEqualsFilter("model.parents.id", getModel().getID());
        terms.addEqualsFilter("model.parents.link.relationType", Category.CHILD);
        return terms;
    }
    
    /**
     * Retrieves all terms in the domain that have no children
     * @return a collection of leaf terms
     */
    public DomainCollection getLeafTerms() {
	DataQuery query = SessionManager.getSession().retrieveQuery("com.arsdigita.london.terms.LeafTerms");
	query.setParameter("domain", getKey());
	return new DomainCollection(new DataQueryDataCollectionAdapter(query, "leaf"));
    }
 
    /**
     * Retrieves any terms not in the hiearchy
     */
    public DomainCollection getOrphanedTerms() {
        DomainCollection terms = getTerms();
        terms.addFilter("model.defaultAncestors = model.id || '/'");
        return terms;        
    }

    
    /**
     * Sets this domain as the root for an object
     * @param obj the object to set the root of
     * @param context the usage context
     */
    public void setAsRootForObject(ACSObject obj,
                                   String context) {
        Category.setRootForObject(obj,
                                  getModel(),
                                  context);
    }
    
    /**
     * Highly experimental. Don't use this
     */
    public DomainCollection getUseContexts() {
        DataCollection objs = SessionManager.getSession()
            .retrieve("com.arsdigita.categorization.UseContext");
        objs.addEqualsFilter("rootCategory.id",
                             getModel().getID());
        return new DomainCollection(objs);
    }
    
    /**
     * Retrieves a query summarizing the item count
     * for each term in the domain
     * @return the term item count summary
     */
    public TermItemCountQuery getTermItemCountSummary() {
        return new TermItemCountQuery(this);
    }

    /**
     * Retrieves the collection of terms in this domain
     * which are associated with the object directly or indirectly.
     * @param obj the object to get terms for
     * @return the collection of terms for this object
     */
    public DomainCollection getAllTerms(ACSObject obj) {
        DomainCollection terms = getTerms();
        terms.addEqualsFilter("model.roTransSubcats.childObjects.id", obj.getID());
        return terms;
    }

    /**
     * Ensure that the domain's terms are orphaned.
     */
    protected void beforeDelete() {
        Category category = getModel();
        DeleteCheckObserver.observe( category );

        category.deleteCategoryAndOrphan();
        super.beforeDelete();
    }
}
