/*
 * Copyright (C) 2002-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.domain;

import com.arsdigita.util.Assert;

import com.arsdigita.persistence.metadata.ObjectType;
import com.arsdigita.persistence.metadata.Property;
import com.arsdigita.persistence.DataCollection;
import com.arsdigita.persistence.DataObject;
import com.arsdigita.persistence.DataAssociation;
import com.arsdigita.persistence.DataAssociationCursor;
import com.arsdigita.persistence.DataQuery;
import com.arsdigita.persistence.DataQueryDataCollectionAdapter;
import com.arsdigita.persistence.OID;
import com.arsdigita.persistence.SessionManager;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.HashSet;

import org.apache.log4j.Logger;

/**
 * <p>This class provides a general purpose framework for iterating
 * over a domain object's properties, processing attributes and
 * traversing associations as required.</p>
 *
 * <p>Subclasses should implement the startXXX and endXXX methods to
 * provide whatever processing logic they require upon encountering
 * attributes, roles, associations and objects.</p>
 *
 * <p>The {@link com.arsdigita.domain.DomainObjectTraversalAdapter}
 * provides a means to control which properties are processed and,
 * most importantly, which associations are traversed. When
 * registering an adapter, a 'use context' is supplied allowing
 * different adapters to be used according to the requirements of any
 * implementing subclass. It is recommended that the use context be
 * based on the fully qualified name of the class using
 * DomainObjectTraversal, e.g.,
 * com.arsdigita.cms.ui.DomainObjectRenderer.</p>
 *
 * <p>The path argument provided to the adapter and the startXXX ad
 * endXXX methods indicates which associations are currently being
 * traversed. The first element in the path is always '/object'. If it
 * then starts to traverse the 'rootCategory' association, the path
 * will become '/object/rootCategory'. For self-recursive
 * associations, rather than building up a long repeating string, the
 * path will be shortened by adding a '+' for each element that is
 * repeated.  For example, '/object/container+' indicates that the
 * container association has been followed two or more times.</p>
 */
public abstract class DomainObjectTraversal {

    private Set m_visited = new HashSet();
    private static Map s_adapters = new HashMap();
    private static final Logger s_log = Logger.getLogger(DomainObjectTraversal.class);

    public final static String LINK_NAME = "link";

    /**
     * Registers a traversal adapter for an object type in a given
     * context.
     *
     * @param type the object type whose items will be traversed
     * @param adapter the adapter for controlling object traversal
     * @param context the context in which the adapter should be used
     */
    public static void registerAdapter(final ObjectType type,
                                       final DomainObjectTraversalAdapter adapter,
                                       final String context) {
        Assert.assertNotNull(adapter, "The DomainObjectTraversalAdapter is null for context '" + context + "' and object type '" + type);
        Assert.assertNotNull(type, "The ObjectType for context '" + context + "' and adapter '" + adapter + "' is null");
        Assert.exists(context, String.class);
        if (s_log.isDebugEnabled()) {
            s_log.debug("Registering adapter " +
                    adapter.getClass() +
                    " for object type " +
                    type.getQualifiedName() +
                    " in context " +
                    context);
        }
        s_adapters.put(new AdapterKey(type, context), adapter);
    }

    /**
     * Unregisteres a traversal adapter for an object type in a
     * given context
     *
     * @param type the object type whose items will be traversed
     * @param context the context in which the adapter should be used
     */
    public static void unregisterAdapter(final ObjectType type,
                                         final String context) {
        Assert.exists(type, ObjectType.class);
        Assert.exists(context, String.class);

        if (s_log.isDebugEnabled()) {
            s_log.debug("Removing adapter " +
                    " for object type " +
                    type.getQualifiedName() +
                    " in context " +
                    context);
        }

        s_adapters.remove(new AdapterKey(type, context));
    }

    /**
     * Registers a traversal adapter for an object type in a given
     * context.
     *
     * @param type the object type whose items will be traversed
     * @param adapter the adapter for controlling object traversal
     * @param context the context in which the adapter should be used
     */
    public static void registerAdapter(final String type,
                                       final DomainObjectTraversalAdapter adapter,
                                       final String context) {
        registerAdapter(SessionManager.getMetadataRoot().getObjectType(type),
                        adapter,
                        context);
    }

    /**
     * Unregisteres a traversal adapter for an object type in a
     * given context
     *
     * @param type the object type whose items will be traversed
     * @param context the context in which the adapter should be used
     */
    public static void unregisterAdapter(final String type,
                                         final String context) {
        unregisterAdapter(SessionManager.getMetadataRoot().getObjectType(type),
                          context);
    }

    /**
     * Retrieves the traversal adapter for an object type in a given
     * context.
     *
     * @param type the object type to lookup
     * @param context the adapter context
     */
    public static DomainObjectTraversalAdapter lookupAdapter(final ObjectType type,
                                                             final String context) {
        Assert.exists(type, ObjectType.class);
        Assert.exists(context, String.class);
        if (s_log.isDebugEnabled()) {
            s_log.debug("lookupAdapter for type "  +
                        type.getQualifiedName() +
                        " in context " +
                        context);

        }

        return (DomainObjectTraversalAdapter)s_adapters
            .get(new AdapterKey(type, context));
    }

    /**
     * Retrieves the closest matching traversal adapter for an object type
     * in a given context. The algorithm looks for an exact match, then
     * considers the supertype, and the supertype's supertype. If no match
     * could be found at all, returns null
     *
     * @param type the object type to search for
     * @param context the adapter context
     */
    public static DomainObjectTraversalAdapter findAdapter(ObjectType type,
                                                           final String context) {
        Assert.exists(type, ObjectType.class);
        Assert.exists(context, String.class);
        if (s_log.isDebugEnabled()) {
            s_log.debug("findAdapter for type "  +
                    type.getQualifiedName() +
                    " in context " +
                    context);

            StringBuffer buf = new StringBuffer();
            buf.append( "Adapters contain:\n" );
            Iterator keys = s_adapters.keySet().iterator();
            while( keys.hasNext() ) {
                Object key = keys.next();
                buf.append( key.toString() ).append( ": " );
                buf.append( s_adapters.get( key ).toString() ).append( '\n' );
            }

            s_log.debug( buf.toString() );
        }
        DomainObjectTraversalAdapter adapter = null;
        ObjectType tmpType = type;
        while (adapter == null && tmpType != null) {
            adapter = lookupAdapter(tmpType, context);
            tmpType = tmpType.getSupertype();
        }
        if (adapter == null) {
            s_log.warn("Could not find adapter for object type " +
                    type.getQualifiedName() +
                    " in context " +
                    context);
        }
        return adapter;
    }

    /**
     * Walks over properties of a domain object, invoking
     * methods to handle assoications, roles and attributes.
     *
     * @param obj the domain object to traverse
     * @param context the context for the traversal adapter
     */
    public void walk(final DomainObject obj,
                     final String context) {
        final DomainObjectTraversalAdapter adapter = findAdapter(obj.getObjectType(),
                                                           context);
        if (adapter == null) {
            final String errorMsg = "No adapter for object " +
                                obj.getOID() +
                                " in context " +
                                context;
            s_log.error(errorMsg);
            throw new IllegalArgumentException(errorMsg);
        }
        walk(obj, context, adapter);
    }

    protected void walk(final DomainObject obj,
                        final String context,
                        final DomainObjectTraversalAdapter adapter) {
        Assert.exists(adapter, DomainObjectTraversalAdapter.class);
        walk(adapter, obj, "/object", context, null);
    }

    private void walk(final DomainObjectTraversalAdapter adapter,
                      final DomainObject obj,
                      final String path,
                      final String context,
                      final DomainObject linkObject) {
        OID oid = obj.getOID();
        OID linkOid = null;
        if (linkObject != null) {
        	linkOid = linkObject.getOID();
        }
        OID[] visitedKey = {oid, linkOid};
        // Prevent infinite recursion
        if (m_visited.contains(visitedKey)) {
            revisitObject(obj, path);
            return;
        } else {
            m_visited.add(visitedKey);
        }

        beginObject(obj, path);

        if (linkObject != null) {
            beginLink(linkObject, path);
            walk(adapter, 
                 linkObject,
                 appendToPath(path, LINK_NAME),
                 context,
                 null);
            endLink(linkObject, path);
        }

        ObjectType type = obj.getObjectType();

        for (Iterator i = type.getProperties(); i.hasNext(); ) {
            Property prop = (Property) i.next();
            String propName = prop.getName();

            if (!adapter.processProperty(obj,
                                         appendToPath(path, prop.getName()),
                                         prop,
                                         context)) {
                if( s_log.isDebugEnabled() ) {
                    s_log.debug( "Not processing " +
                                 appendToPath( path, prop.getName() ) +
                                 " in object " + oid + " and context " +
                                 context + " with adapter " +
                                 adapter.getClass().getName() );
                }
                continue;
            }
            Object propValue = obj.get(propName);
            if (propValue == null) {
                if( s_log.isDebugEnabled() ) {
                    s_log.debug( "Object " + oid.toString() + " doesn't " +
                                 "contain property " + propName );
                }
                continue;
            }

            if (prop.isAttribute()) {
                handleAttribute(obj, path, prop);
            } else if (propValue instanceof DataObject) {
                if( s_log.isDebugEnabled() ) {
                    s_log.debug( prop.getName() + " is a DataObject" );
                }

                beginRole(obj, path, prop);

                walk(adapter, 
                     DomainObjectFactory.newInstance((DataObject)propValue),
                     appendToPath(path, propName),
                     context,
                     null);

                endRole(obj, path, prop);
            } else if (propValue instanceof DataAssociation) {
            	
            	
            	// see #25808 - this hack prevents the content field of cms_files 
            	// (which is a blob) from being queried when all we need is a list
            	// of the files on an item..
            	if (prop.getName().equals("fileAttachments") && !Domain.getConfig().queryBlobContentForFileAttachments()) { // make true a config
            		DataQuery fileAttachmentsQuery = SessionManager.getSession().retrieveQuery("com.arsdigita.cms.contentassets.fileAttachmentsQuery");
            		
            		fileAttachmentsQuery.setParameter("item_id", obj.getOID().get("id"));
            		
            		DataCollection files = new DataQueryDataCollectionAdapter(fileAttachmentsQuery, "file");
            		
            		while(files.next()) {
            			DataObject file = files.getDataObject();
            			walk(adapter, 
                                DomainObjectFactory.newInstance
                                (file),
                                appendToPath(path, propName),
                                context,
                                null);
            		}
            		
            	} else { 
            	
                if( s_log.isDebugEnabled() ) {
                    s_log.debug( prop.getName() + " is a DataAssociation" );
                }
                beginAssociation(obj, path, prop);
                
                DataAssociationCursor daCursor =
                    ((DataAssociation)propValue).getDataAssociationCursor();
                
                while (daCursor.next()) {
                    DataObject link = daCursor.getLink();
                    DomainObject linkObj = null;
                    if (link != null) {
                        linkObj = new LinkDomainObject(link);
                    }
                    walk(adapter, 
                         DomainObjectFactory.newInstance
                         (daCursor.getDataObject()),
                         appendToPath(path, propName),
                         context,
                         linkObj);
                }
                endAssociation(obj, path, prop);
            	}
            } else {
                // Unknown property value type - do nothing
            }
        }

        endObject(obj, path);
    }


    /**
     * Method called when the processing of an object
     * starts
     */
    protected abstract void beginObject(DomainObject obj,
                                        String path);
    /**
     * Method called when the procesing of an object
     * completes
     */
    protected abstract void endObject(DomainObject obj,
                                      String path);

    /**
     * Method called when the processing of a Link Object
     * starts
     */
    protected void beginLink(DomainObject obj, String path) {}
    /**
     * Method called when the procesing of a Link Object
     * completes
     */
    protected void endLink(DomainObject obj, String path) {}

    /**
     * Method called when a previously visited object
     * is encountered for a second time.
     */
    protected abstract void revisitObject(DomainObject obj,
                                          String path);

    /**
     * Method called when an attribute is encountered
     */
    protected abstract void handleAttribute(DomainObject obj,
                                            String path,
                                            Property property);

    /**
     * Method called when the processing of a role
     * starts
     */
    protected abstract void beginRole(DomainObject obj,
                                      String path,
                                      Property property);

    /**
     * Method called when the procesing of a role
     * completes
     */
    protected abstract void endRole(DomainObject obj,
                                    String path,
                                    Property property);

    /**
     * Method called when the processing of an association
     * starts
     */
    protected abstract void beginAssociation(DomainObject obj,
                                             String path,
                                             Property property);

    /**
     * Method called when the procesing of an association
     * completes
     */
    protected abstract void endAssociation(DomainObject obj,
                                           String path,
                                           Property property);


    protected String appendToPath(String path,
                                  String name) {
        if (path.endsWith("/" + name)) {
            path = path + "+";
        } else if (!path.endsWith("/" + name + "+")) {
            path = path + "/" + name;
        }
        
        return path;
    }

    protected String nameFromPath(String path) {
        int index = path.lastIndexOf("/");
        Assert.truth(index >= 0, "Path starts with /");

        if (path.endsWith("+")) {
            return path.substring(index + 1, path.length() - 1);
        } else {
            return path.substring(index + 1);
        }
    }

    protected String parentFromPath(String path) {
        int index = path.lastIndexOf("/");
        Assert.truth(index >= 0, "Path starts with /");

        if (index == 0) {
            return null;
        } else {
            return path.substring(0, index - 1);
        }
    }


    protected static class AdapterKey {
        private final ObjectType m_type;
        private final String m_context;

        public AdapterKey(ObjectType type,
                          String context) {
            Assert.exists(type, ObjectType.class);
            Assert.exists(context, String.class);
            m_type = type;
            m_context = context;
        }

        public boolean equals(Object o) {
            if (o instanceof AdapterKey) {
                AdapterKey k = (AdapterKey)o;
                return k.m_type.equals(m_type) &&
                    k.m_context.equals(m_context);
            } else {
                return false;
            }
        }

        public int hashCode() {
            return m_type.hashCode() + m_context.hashCode();
        }

        public String toString() {
            return m_type.getQualifiedName() + ',' + m_context;
        }
    }

    /**
     *  this is simply a subclass since DomainObject is abstract
     *  but we don't have any other domain object to use.
     */
    private class LinkDomainObject extends DomainObject {
        LinkDomainObject(DataObject object) {
            super(object);
        }
    }

}
