/*
 * 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.brooklyn.core.entity.factory;

import static com.google.common.base.Preconditions.checkNotNull;

import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.mgmt.EntityManager;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.StartableApplication;
import org.apache.brooklyn.entity.stock.BasicApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.Beta;

/**
 * Experimental mechanism for defining/building applications. In future releases, this
 * API will change. Its concepts will most likely be merged with a TOSCA implementation
 * and with {@link EntitySpec}.
 *
 * For building an application. Users can sub-class and override doBuild(), putting the logic for  
 * creating and wiring together entities in there.
 * 
 * The builder is mutable; a given instance should be used to build only a single application.
 * Once {@link #manage()} has been called, the application will be built and no additional configuration
 * should be performed through this builder.  
 * 
 * Example (simplified) code for sub-classing is:
 * <pre>
 * {@code
 *   app = new ApplicationBuilder() {
 *       //@Override
 *       public void doBuild() {
 *           MySqlNode db = addChild(EntitySpec.create(MySqlNode.class)));
 *           JBoss7Server as = addChild(EntitySpec.create(JBoss7Server.class)
 *                   .configure(HTTP_PORT, "8080+")
 *                   .configure(javaSysProp("brooklyn.example.db.url"), attributeWhenReady(db, MySqlNode.MYSQL_URL));
 *       }
 *   }.manage();
 * }
 * </pre>
 * 
 * @author aled
 * 
 * @deprecated since 0.9.0; use {@link EntitySpec} and {@link EntityManager#createEntity(EntitySpec)}, having 
 *             added the children to the spec etc.
 */
@Deprecated
@Beta
public abstract class ApplicationBuilder {

    @SuppressWarnings("unused")
    private static final Logger LOG = LoggerFactory.getLogger(ApplicationBuilder.class);

    @SuppressWarnings("unchecked")
    @Beta
    /** @deprecated since 0.7.0 the management context should normally be passed in;
     * for TestApplication also see TestApplication.Factory.newManagedInstanceForTests() */ 
    @Deprecated
    public static <T extends StartableApplication> T newManagedApp(Class<T> type) {
        if (type.isInterface()) {
            return newManagedApp(EntitySpec.create(type));
        } else {
            return (T) newManagedApp(EntitySpec.create(StartableApplication.class, type));
        }
    }

    @SuppressWarnings("unchecked")
    private static <T extends StartableApplication> T newManagedApp(EntitySpec<T> spec) {
        return (T) new ApplicationBuilder(spec) {
            @Override protected void doBuild() {
            }
        }.manage();
    }

    @SuppressWarnings("unchecked")
    @Beta
    public static <T extends StartableApplication> T newManagedApp(Class<T> type, ManagementContext managementContext) {
        if (type.isInterface()) {
            return newManagedApp(EntitySpec.create(type), managementContext);
        } else {
            return (T) newManagedApp(EntitySpec.create(StartableApplication.class, type), managementContext);
        }
    }

    /** @deprecated class can be removed; users of this convenience method can now simply do mgmt.getEntityManager().createEntity(spec) */ 
    @Deprecated
    @SuppressWarnings("unchecked")
    @Beta
    public static <T extends StartableApplication> T newManagedApp(EntitySpec<T> spec, ManagementContext managementContext) {
        return (T) new ApplicationBuilder(spec) {
            @Override protected void doBuild() {
            }
        }.manage(managementContext);
    }

    protected volatile boolean managed = false;
    protected final AtomicBoolean inManage = new AtomicBoolean(false);
    private EntitySpec<? extends StartableApplication> appSpec;
    private ManagementContext managementContext;
    private StartableApplication app;
    
    public ApplicationBuilder() {
        this.appSpec = EntitySpec.create(BasicApplication.class);
    }

    public ApplicationBuilder(EntitySpec<? extends StartableApplication> appSpec) {
        this.appSpec = EntitySpec.create(appSpec);
    }

    public final ApplicationBuilder appDisplayName(String val) {
        checkPreManage();
        appSpec.displayName(val);
        return this;
    }
    
    protected final <T extends Entity> T createEntity(EntitySpec<T> spec) {
        checkDuringManage();
        EntityManager entityManager = managementContext.getEntityManager();
        return entityManager.createEntity(spec);
    }

    /**
     * Adds the given entity as a child of the application being built.
     * To be called during {@link #doBuild()}.
     */
    protected final <T extends Entity> T addChild(T entity) {
        checkDuringManage();
        return app.addChild(entity);
    }

    /**
     * Returns the type of the application being built.
     */
    public final Class<? extends StartableApplication> getType() {
        return appSpec.getType();
    }
    
    /**
     * Configures the application instance.
     */
    public final ApplicationBuilder configure(Map<?,?> config) {
        checkPreManage();
        appSpec.configure(config);
        return this;
    }
    
    /**
     * Adds the given entity as a child of the application being built.
     */
    protected final <T extends Entity> T addChild(EntitySpec<T> spec) {
        checkDuringManage();
        return addChild(createEntity(spec));
    }
    
    protected final <T extends Entity> T addChild(Map<?,?> config, Class<T> type) {
        checkDuringManage();
        EntitySpec<T> spec = EntitySpec.create(type).configure(config);
        return addChild(createEntity(spec));
    }
    
    protected final ManagementContext getManagementContext() {
        return checkNotNull(managementContext, "must only be called after manage()");
    }

    protected final StartableApplication getApp() {
        return checkNotNull(app, "must only be called after manage()");
    }

    /**
     * For overriding, to create and wire together entities.
     */
    protected abstract void doBuild();

    /**
     * Creates a new {@link ManagementContext}, and then builds and manages the application.
     * 
     * @see #manage(ManagementContext)
     */
    public final StartableApplication manage() {
        return manage(Entities.newManagementContext());
    }
    
    /**
     * Builds and manages the application, calling the user's {@link #doBuild()} method.
     * 
     * @throws IllegalStateException If already managed, or if called during {@link #doBuild()}, or if 
     *                               multiple concurrent calls
     */
    public final StartableApplication manage(ManagementContext managementContext) {
        if (!inManage.compareAndSet(false, true)) {
            throw new IllegalStateException("Concurrent and re-entrant calls to manage() forbidden on "+this);
        }
        try {
            checkNotManaged();
            this.managementContext = managementContext;
            this.app = managementContext.getEntityManager().createEntity(appSpec);
            doBuild();
            // not needed with 0.9.0 (TODO - remove when confirmed)
//            Entities.startManagement(app, managementContext);
            managed = true;
            return app;
        } finally {
            inManage.set(false);
        }
    }
    
    protected void checkPreManage() {
        if (inManage.get()) {
            throw new IllegalStateException("Builder being managed; cannot perform operation during call to manage(), or in doBuild()");
        }
        if (managed) {
            throw new IllegalStateException("Builder already managed; cannot perform operation after call to manage()");
        }
    }
    
    protected void checkNotManaged() {
        if (managed) {
            throw new IllegalStateException("Builder already managed; cannot perform operation after call to manage()");
        }
    }
    
    protected void checkDuringManage() {
        if (!inManage.get() || app == null) {
            throw new IllegalStateException("Operation only permitted during manage, e.g. called from doBuild() of "+this);
        }
    }
}
