/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 ****************************************************************/
package org.apache.cayenne.reflect;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.map.EntityResolver;

/**
 * An object that holds class descriptors for mapped entities, compiling new descriptors
 * on demand using an internal chain of descriptor factories. Note that the object is ot
 * synchronized internally, so it has to be prefilled with descriptors by the caller on
 * initialization via calling 'getDescriptor' for all mapped entities.
 * 
 * @since 3.0
 * @author Andrus Adamchik
 */
public class ClassDescriptorMap {

    protected EntityResolver resolver;
    protected Map<String, ClassDescriptor> descriptors;
    protected List<ClassDescriptorFactory> factories;

    public ClassDescriptorMap(EntityResolver resolver) {
        this.descriptors = new HashMap<String, ClassDescriptor>();
        this.resolver = resolver;
        this.factories = new ArrayList<ClassDescriptorFactory>();
    }

    public EntityResolver getResolver() {
        return resolver;
    }

    /**
     * Adds a factory to the descriptor factory chain.
     */
    public void addFactory(ClassDescriptorFactory factory) {
        factories.add(factory);
    }

    public void removeFactory(ClassDescriptorFactory factory) {
        factories.remove(factory);
    }

    public void clearFactories() {
        factories.clear();
    }

    public void clearDescriptors() {
        descriptors.clear();
    }

    /**
     * Removes cached descriptor if any for the given entity.
     */
    public void removeDescriptor(String entityName) {
        descriptors.remove(entityName);
    }

    /**
     * Caches descriptor definition.
     */
    public void addDescriptor(String entityName, ClassDescriptor descriptor) {
        if (descriptor == null) {
            removeDescriptor(entityName);
        }
        else {
            descriptors.put(entityName, descriptor);
        }
    }

    public ClassDescriptor getDescriptor(String entityName) {
        if (entityName == null) {
            throw new NullPointerException("Null 'entityName'");
        }

        ClassDescriptor cached = descriptors.get(entityName);
        if (cached != null) {
            return cached;
        }

        return createProxyDescriptor(entityName);
    }

    /**
     * Creates a descriptor wrapper that will compile the underlying descriptor on demand.
     * Using proxy indirection is needed to compile relationships of descriptors to other
     * descriptors that are not compiled yet.
     */
    protected ClassDescriptor createProxyDescriptor(String entityName) {
        ClassDescriptor descriptor = new LazyClassDescriptorDecorator(this, entityName);
        addDescriptor(entityName, descriptor);
        return descriptor;
    }

    /**
     * Creates a new descriptor.
     */
    protected ClassDescriptor createDescriptor(String entityName) {

        // scan the factory chain until some factory returns a non-null descriptor;
        // scanning is done in reverse order so that the factories added last take higher
        // precedence...
        ListIterator<ClassDescriptorFactory> it = factories
                .listIterator(factories.size());
        while (it.hasPrevious()) {
            ClassDescriptorFactory factory = it.previous();
            ClassDescriptor descriptor = factory.getDescriptor(entityName);

            if (descriptor != null) {
                return descriptor;
            }
        }

        throw new CayenneRuntimeException("Failed to create descriptor for entity: "
                + entityName);
    }
}
