/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.naming;

import org.jboss.as.naming.deployment.JndiNamingDependencyProcessor;
import org.jboss.as.naming.deployment.RuntimeBindReleaseService;
import org.jboss.as.naming.service.BinderService;
import org.jboss.as.naming.util.ThreadLocalStack;
import org.jboss.msc.service.ServiceBuilder;
import org.jboss.msc.service.ServiceController;
import org.jboss.msc.service.ServiceName;
import org.jboss.msc.service.ServiceRegistry;
import org.jboss.msc.service.ServiceTarget;
import org.jboss.msc.service.StabilityMonitor;
import org.jboss.msc.value.ImmediateValue;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NamingException;
import java.util.Hashtable;

import static org.jboss.as.naming.NamingMessages.MESSAGES;
import static org.jboss.as.naming.util.NamingUtils.isLastComponentEmpty;
import static org.jboss.as.naming.util.NamingUtils.namingException;

/**
 * @author John Bailey
 * @author Eduardo Martins
 * @author <a href="mailto:ropalka@redhat.com">Richard Opalka</a>
 */
public class WritableServiceBasedNamingStore extends ServiceBasedNamingStore implements WritableNamingStore {
    private static final ThreadLocalStack WRITE_OWNER = new ThreadLocalStack();

    private final ServiceTarget serviceTarget;

    public WritableServiceBasedNamingStore(ServiceRegistry serviceRegistry, ServiceName serviceNameBase, ServiceTarget serviceTarget) {
        super(serviceRegistry, serviceNameBase);
        this.serviceTarget = serviceTarget;
    }

    public void bind(final Name name, final Object object, final Class<?> bindType) throws NamingException {
        bind(name, object);
    }

    public void bind(final Name name, final Object object) throws NamingException {
        final Object owner = requireOwner();
        final ServiceName bindName = buildServiceName(name);
        bind(name, object, owner, bindName);
    }

    private void bind(Name name, Object object, Object owner, ServiceName bindName) throws NamingException {
        ServiceTarget serviceTarget = this.serviceTarget;
        ServiceName deploymentUnitServiceName = null;
        if (owner instanceof ServiceName) {
            deploymentUnitServiceName = (ServiceName) owner;
        } else {
            serviceTarget = (ServiceTarget) owner;
        }
        try {
            // unlike on deployment processors, we may assume here it's a shareable bind if the owner is a deployment, because deployment unshareable namespaces are readonly stores
            final BinderService binderService = new BinderService(name.toString(), null, deploymentUnitServiceName != null);
            final ServiceBuilder<?> builder = serviceTarget.addService(bindName, binderService)
                    .addDependency(getServiceNameBase(), ServiceBasedNamingStore.class, binderService.getNamingStoreInjector())
                    .addInjection(binderService.getManagedObjectInjector(), new ImmediateManagedReferenceFactory(object))
                    .setInitialMode(ServiceController.Mode.ACTIVE);
            final ServiceController<?> binderServiceController = builder.install();
            final StabilityMonitor monitor = new StabilityMonitor();
            monitor.addController(binderServiceController);
            try {
                monitor.awaitStability();
            } finally {
                monitor.removeController(binderServiceController);
            }
            final Exception startException = binderServiceController.getStartException();
            if (startException != null) {
                throw startException;
            }
            if (deploymentUnitServiceName != null) {
                binderService.acquire();
                final RuntimeBindReleaseService.References duBindingReferences = (RuntimeBindReleaseService.References) binderServiceController.getServiceContainer().getService(JndiNamingDependencyProcessor.serviceName(deploymentUnitServiceName)).getValue();
                duBindingReferences.add(binderService);
            }
        } catch (Exception e) {
            throw namingException("Failed to bind [" + object + "] at location [" + bindName + "]", e);
        }
    }

    public void rebind(Name name, Object object) throws NamingException {
        final Object owner = requireOwner();
        // re-set the existent binder service injected value
        final ServiceName bindName = buildServiceName(name);
        final ServiceController<?> controller = getServiceRegistry().getService(bindName);
        if (controller == null) {
            bind(name, object, owner, bindName);
        } else {
            final BinderService binderService = (BinderService) controller.getService();
            if (owner instanceof ServiceName) {
                final ServiceName deploymentUnitServiceName = (ServiceName) owner;
                binderService.acquire();
                final RuntimeBindReleaseService.References duBindingReferences = (RuntimeBindReleaseService.References) controller.getServiceContainer().getService(JndiNamingDependencyProcessor.serviceName(deploymentUnitServiceName)).getValue();
                duBindingReferences.add(binderService);
            }
            binderService.getManagedObjectInjector().setValue(new ImmediateValue(new ImmediateManagedReferenceFactory(object)));
        }
    }

    public void rebind(final Name name, final Object object, final Class<?> bindType) throws NamingException {
        rebind(name, object);
    }

    public void unbind(final Name name) throws NamingException {
        requireOwner();
        final ServiceName bindName = buildServiceName(name);
        final ServiceController<?> controller = getServiceRegistry().getService(bindName);
        if (controller == null) {
            throw MESSAGES.cannotResolveService(bindName);
        }
        controller.setMode(ServiceController.Mode.REMOVE);
        final StabilityMonitor monitor = new StabilityMonitor();
        monitor.addController(controller);
        try {
            monitor.awaitStability();
        } catch (Exception e) {
            throw namingException("Failed to unbind [" + bindName + "]", e);
        } finally {
            monitor.removeController(controller);
        }
    }

    public Context createSubcontext(final Name name) throws NamingException {
        requireOwner();
        if (isLastComponentEmpty(name)) {
            throw MESSAGES.emptyNameNotAllowed();
        }
        return new NamingContext(name, WritableServiceBasedNamingStore.this, new Hashtable<String, Object>());
    }

    private Object requireOwner() {
        final Object owner = WRITE_OWNER.peek();
        if (owner == null) {
            throw MESSAGES.readOnlyNamingContext();
        }
        return owner;
    }

    public static void pushOwner(final ServiceName deploymentUnitServiceName) {
        WRITE_OWNER.push(deploymentUnitServiceName);
    }

    public static void pushOwner(final ServiceTarget serviceTarget) {
        WRITE_OWNER.push(serviceTarget);
    }

    public static void popOwner() {
        WRITE_OWNER.pop();
    }

}
