/*
 * 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.felix.dm.impl;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import org.apache.felix.dm.DependencyActivatorBase;
import org.apache.felix.dm.ServiceDependency;
import org.apache.felix.dm.context.DependencyContext;
import org.apache.felix.dm.context.EventType;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;

/**
 * Temporal Service dependency implementation, used to hide temporary service dependency "outage".
 * Only works with a required dependency.
 *
 * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
 */
public class TemporalServiceDependencyImpl extends ServiceDependencyImpl implements ServiceDependency, InvocationHandler {
    // Max millis to wait for service availability.
    private final long m_timeout;
        
    // Framework bundle (we use it to detect if the framework is stopping)
    private final Bundle m_frameworkBundle;

    // The service proxy, which blocks when service is not available.
    private volatile Object m_serviceInstance;

    /**
     * Creates a new Temporal Service Dependency.
     * 
     * @param context The bundle context of the bundle which is instantiating this dependency object
     * @param logger the logger our Internal logger for logging events.
     * @see DependencyActivatorBase#createTemporalServiceDependency()
     */
    public TemporalServiceDependencyImpl(BundleContext context, long timeout) {
        super.setRequired(true);
        if (timeout < 0) {
            throw new IllegalArgumentException("Invalid timeout value: " + timeout);
        }
        m_timeout = timeout;
        m_frameworkBundle = context.getBundle(0);
    }
    
    /**
     * Creates a clone of an existing temporal service dependency.
     */
    public TemporalServiceDependencyImpl(TemporalServiceDependencyImpl prototype) {
        super(prototype);
        super.setRequired(true);
        m_timeout = prototype.m_timeout;
        m_frameworkBundle = prototype.m_frameworkBundle;
    }

    @Override
    public DependencyContext createCopy() {
        return new TemporalServiceDependencyImpl(this);
    }

    /**
     * Sets the required flag which determines if this service is required or not. This method
     * just override the superclass method in order to check if the required flag is true 
     * (optional dependency is not supported by this class).
     * 
     * @param required the required flag, which must be set to true
     * @return this service dependency
     * @throws IllegalArgumentException if the "required" parameter is not true.
     */
    @Override
    public ServiceDependency setRequired(boolean required) {
        if (! required) {
            throw new IllegalArgumentException("A Temporal Service dependency can't be optional");
        }
        super.setRequired(required);
        return this;
    }
    
    /**
     * The ServiceTracker calls us here in order to inform about a service arrival.
     */
	@SuppressWarnings("rawtypes")
	@Override
    public void addedService(ServiceReference ref, Object event) {    	
        // Update our service cache, using the tracker. We do this because the
        // just added service might not be the service with the highest rank ...
        boolean makeAvailable = false;
        synchronized (this) {
            if (m_serviceInstance == null) {
                m_serviceInstance = Proxy.newProxyInstance(m_trackedServiceName.getClassLoader(), new Class[] { m_trackedServiceName }, this);
                makeAvailable = true;
            }
        }
        if (makeAvailable) {
            getComponentContext().handleEvent(this, EventType.ADDED, 
            		 new ServiceEventImpl(m_component, ref, m_serviceInstance));
        } else {
            // This added will possibly unblock our invoke() method (if it's blocked in m_tracker.waitForService method).
        }
    }

    /**
     * The ServiceTracker calls us here when a tracked service properties are modified.
     */
    @SuppressWarnings("rawtypes")
	@Override
    public void modifiedService(ServiceReference ref, Object service) {
        // We don't care.
    }

    /**
     * The ServiceTracker calls us here when a tracked service is lost.
     */
    @SuppressWarnings("rawtypes")
	@Override
    public void removedService(ServiceReference ref, Object event) {
    	ServiceEventImpl eventImpl = (ServiceEventImpl) event;
    	
    	// If we detect that the fwk is stopping, we behave as our superclass. That is:
        // the lost dependency has to trigger our service deactivation, since the fwk is stopping
        // and the lost dependency won't come up anymore.
        if (m_frameworkBundle.getState() == Bundle.STOPPING) {
            // Important: Notice that calling "super.removedService() might invoke our service "stop" 
            // callback, which in turn might invoke the just removed service dependency. In this case, 
            // our "invoke" method won't use the tracker to get the service dependency (because at this point, 
            // the tracker has withdrawn its reference to the lost service). So, you will see that the "invoke" 
            // method will use the "m_cachedService" instead ...
            boolean makeUnavailable = false;
            synchronized (this) {
                if (m_tracker.getService() == null) {
                    makeUnavailable = true;
                }
            }
            if (makeUnavailable) {
                // the event.close method will unget the service.
                m_component.handleEvent(this, EventType.REMOVED, new ServiceEventImpl(m_component, ref, m_serviceInstance));
            }
        } else {
        	eventImpl.close(); // will unget the service.                	
        	// if there is no available services, the next call to invoke() method will block until another service
        	// becomes available. Else the next call to invoke() will return that highest ranked available service.            
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        ServiceEventImpl event = null;
        try {
            event = (ServiceEventImpl) m_tracker.waitForService(m_timeout);
        } catch (InterruptedException e) {            
        }
        
        if (event == null) {
            throw new IllegalStateException("Service unavailable: " + m_trackedServiceName.getName());
        }
        
        Object service = event.getEvent();
        if (service == null) {
            throw new IllegalStateException("Service unavailable: " + m_trackedServiceName.getName());
        }       
        
        try {
			try {
				return method.invoke(service, args);
			} catch (IllegalAccessException iae) {
				method.setAccessible(true);
				return method.invoke(service, args);
			}
        } catch (InvocationTargetException e) {
        	throw e.getTargetException();
        }
    }
}
