/**
 *
 * 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.aries.blueprint.di;

import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

import org.apache.aries.blueprint.container.GenericType;
import org.osgi.service.blueprint.container.ComponentDefinitionException;
import org.osgi.service.blueprint.container.ReifiedType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AbstractRecipe implements Recipe {
    private static final Logger LOGGER = LoggerFactory
            .getLogger(AbstractRecipe.class);

    protected final String name;
    protected boolean prototype = true;

    protected AbstractRecipe(String name) {
        if (name == null)
            throw new NullPointerException("name is null");
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public boolean isPrototype() {
        return prototype;
    }

    public void setPrototype(boolean prototype) {
        this.prototype = prototype;
    }

    public final Object create() throws ComponentDefinitionException {
        // Ensure a container has been set
        ExecutionContext context = ExecutionContext.Holder.getContext();

        // if this recipe has already been executed in this context, return the
        // currently registered value
        Object result = context.getPartialObject(name);
        if (result != null) {
            return result;
        }

        // execute the recipe
        context.push(this);
        boolean didCreate = false;
        try {
            if (!prototype) {
                FutureTask<Object> objectCreation = new FutureTask<Object>(
                        new Callable<Object>() {
                            public Object call()
                                    throws ComponentDefinitionException {
                                return internalCreate();
                            }
                        });
                Future<Object> resultFuture = context.addFullObject(name,
                        objectCreation);

                // are we the first to try to create it
                if (resultFuture == null) {
                    didCreate = true;
                    objectCreation.run();
                    resultFuture = objectCreation;
                }

                try {
                    result = resultFuture.get();
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                } catch (ExecutionException ee) {
                    if (ee.getCause() instanceof ComponentDefinitionException)
                        throw (ComponentDefinitionException) ee.getCause();
                    else if (ee.getCause() instanceof RuntimeException)
                        throw (RuntimeException) ee.getCause();
                    else
                        throw (Error) ee.getCause();
                }

            } else {
                result = internalCreate();
            }
        } finally {
            if (didCreate)
                context.removePartialObject(name);

            Recipe popped = context.pop();
            if (popped != this) {
                // noinspection ThrowFromFinallyBlock
                throw new IllegalStateException(
                        "Internal Error: recipe stack is corrupt:"
                                + " Expected " + this
                                + " to be popped of the stack but was "
                                + popped);
            }
        }

        return result;
    }

    protected abstract Object internalCreate()
            throws ComponentDefinitionException;

    protected void addPartialObject(Object obj) {
        if (!prototype) {
            ExecutionContext.Holder.getContext().addPartialObject(name, obj);
        }
    }

    protected boolean canConvert(Object obj, ReifiedType type) {
        return ExecutionContext.Holder.getContext().canConvert(obj, type);
    }

    protected Object convert(Object obj, ReifiedType type) throws Exception {
        return ExecutionContext.Holder.getContext().convert(obj, type);
    }

    protected Object convert(Object obj, Type type) throws Exception {
        return ExecutionContext.Holder.getContext().convert(obj,
                new GenericType(type));
    }

    protected Class loadClass(String className) {
        ReifiedType t = loadType(className, null);
        return t != null ? t.getRawClass() : null;
    }

    protected ReifiedType loadType(String typeName) {
        return loadType(typeName, null);
    }

    protected ReifiedType loadType(String typeName, ClassLoader fromClassLoader) {
        if (typeName == null) {
            return null;
        }
        return doLoadType(typeName, fromClassLoader, true, false);
    }

    private ReifiedType doLoadType(String typeName,
            ClassLoader fromClassLoader, boolean checkNestedIfFailed,
            boolean retry) {
        try {
            return GenericType.parse(typeName,
                    fromClassLoader != null ? fromClassLoader
                            : ExecutionContext.Holder.getContext());
        } catch (ClassNotFoundException e) {
            String errorMessage = "Unable to load class " + typeName
                    + " from recipe " + this;
            if (checkNestedIfFailed) {
                int lastDot = typeName.lastIndexOf('.');
                if (lastDot > 0 && lastDot < typeName.length()) {
                    String nestedTypeName = typeName.substring(0, lastDot)
                            + "$" + typeName.substring(lastDot + 1);
                    LOGGER.debug(errorMessage
                            + ", trying to load a nested class "
                            + nestedTypeName);
                    try {
                        return doLoadType(nestedTypeName, fromClassLoader,
                                false, true);
                    } catch (ComponentDefinitionException e2) {
                        // ignore, the recursive call will throw this exception,
                        // but ultimately the exception referencing the original
                        // typeName has to be thrown
                    }
                }
            }
            if (!retry) {
                LOGGER.error(errorMessage);
            }
            throw new ComponentDefinitionException(errorMessage, e);
        }
    }

    public void destroy(Object instance) {
    }

    public List<Recipe> getConstructorDependencies() {
        return Collections.emptyList();
    }

    public String toString() {
        return getClass().getSimpleName() + "[" + "name='" + name + '\'' + ']';

    }

}
