/*
 * 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.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;

import org.apache.felix.dm.Component;
import org.apache.felix.dm.ComponentDeclaration;
import org.apache.felix.dm.ServiceDependency;
import org.apache.felix.dm.context.AbstractDependency;
import org.apache.felix.dm.context.DependencyContext;
import org.apache.felix.dm.context.Event;
import org.apache.felix.dm.context.EventType;
import org.apache.felix.dm.tracker.ServiceTracker;
import org.apache.felix.dm.tracker.ServiceTrackerCustomizer;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceObjects;
import org.osgi.framework.ServiceReference;

/**
 * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
 */
public class ServiceDependencyImpl extends AbstractDependency<ServiceDependency> implements ServiceDependency, ServiceTrackerCustomizer {
	protected volatile ServiceTracker m_tracker;
    protected volatile String m_swap;
    protected volatile Class<?> m_trackedServiceName;
    private volatile String m_trackedServiceFilter;
    private volatile String m_trackedServiceFilterUnmodified;
    private volatile ServiceReference<?> m_trackedServiceReference;
    private volatile Object m_defaultImplementation;
    private volatile Object m_defaultImplementationInstance;
    private volatile Object m_nullObject;
    private volatile boolean m_debug = false;
    private volatile String m_debugKey;
    private volatile long m_trackedServiceReferenceId;
	private volatile boolean m_obtainServiceBeforeInjection = true;
    
    public ServiceDependency setDebug(String debugKey) {
    	m_debugKey = debugKey;
    	m_debug = true;
    	return this;
    }

    /**
     * Entry to wrap service properties behind a Map.
     */
    private static final class ServicePropertiesMapEntry implements Map.Entry<String, Object> {
        private final String m_key;
        private Object m_value;

        public ServicePropertiesMapEntry(String key, Object value) {
            m_key = key;
            m_value = value;
        }

        public String getKey() {
            return m_key;
        }

        public Object getValue() {
            return m_value;
        }

        public String toString() {
            return m_key + "=" + m_value;
        }

        public Object setValue(Object value) {
            Object oldValue = m_value;
            m_value = value;
            return oldValue;
        }

        @SuppressWarnings("unchecked")
        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry)) {
                return false;
            }
            Map.Entry<String, Object> e = (Map.Entry<String, Object>) o;
            return eq(m_key, e.getKey()) && eq(m_value, e.getValue());
        }

        public int hashCode() {
            return ((m_key == null) ? 0 : m_key.hashCode()) ^ ((m_value == null) ? 0 : m_value.hashCode());
        }

        private static final boolean eq(Object o1, Object o2) {
            return (o1 == null ? o2 == null : o1.equals(o2));
        }
    }

    /**
     * Wraps service properties behind a Map.
     */
    private final static class ServicePropertiesMap extends AbstractMap<String, Object> {
        private final ServiceReference<?> m_ref;

        public ServicePropertiesMap(ServiceReference<?> ref) {
            m_ref = ref;
        }

        public Object get(Object key) {
            return m_ref.getProperty(key.toString());
        }

        public int size() {
            return m_ref.getPropertyKeys().length;
        }

        public Set<Map.Entry<String, Object>> entrySet() {
            Set<Map.Entry<String, Object>> set = new HashSet<>();
            String[] keys = m_ref.getPropertyKeys();
            for (int i = 0; i < keys.length; i++) {
                set.add(new ServicePropertiesMapEntry(keys[i], m_ref.getProperty(keys[i])));
            }
            return set;
        }
    }
    
	public ServiceDependencyImpl() {
	}
	
	public ServiceDependencyImpl(ServiceDependencyImpl prototype) {
		super(prototype);
        m_trackedServiceName = prototype.m_trackedServiceName;
        m_nullObject = prototype.m_nullObject;
        m_trackedServiceFilter = prototype.m_trackedServiceFilter;
        m_trackedServiceFilterUnmodified = prototype.m_trackedServiceFilterUnmodified;
        m_trackedServiceReference = prototype.m_trackedServiceReference;
        m_autoConfigInstance = prototype.m_autoConfigInstance;
        m_defaultImplementation = prototype.m_defaultImplementation;
        m_autoConfig = prototype.m_autoConfig;
        m_obtainServiceBeforeInjection = prototype.m_obtainServiceBeforeInjection;
	}
	    	    
    // --- CREATION
    			
    public ServiceDependency setCallbacks(Object instance, String added, String changed, String removed, String swapped) {
        setCallbacks(instance, added, changed, removed);
        m_swap = swapped;
        return this;
    }
    
    public ServiceDependency setCallbacks(String added, String changed, String removed, String swapped) {
        setCallbacks(added, changed, removed);
        m_swap = swapped;
        return this;
    }
		
    @Override
    public ServiceDependency setDefaultImplementation(Object implementation) {
        ensureNotActive();
        m_defaultImplementation = implementation;
        return this;
    }   

    @Override
   	public ServiceDependency setService(Class<?> serviceName) {
        setService(serviceName, null, null);
        return this;
    }

    public ServiceDependency setService(Class<?> serviceName, String serviceFilter) {
        setService(serviceName, null, serviceFilter);
        return this;
    }

    public ServiceDependency setService(String serviceFilter) {
        if (serviceFilter == null) {
            throw new IllegalArgumentException("Service filter cannot be null.");
        }
        setService(null, null, serviceFilter);
        return this;
    }

    @SuppressWarnings("rawtypes")
	public ServiceDependency setService(Class<?> serviceName, ServiceReference serviceReference) {
        setService(serviceName, serviceReference, null);
        return this;
    }
    
    @Override
    public ServiceDependency setDereference(boolean obtainServiceBeforeInjection) {
    	m_obtainServiceBeforeInjection = obtainServiceBeforeInjection;
    	return this;
    }

	@Override
	public void start() {
        if (m_trackedServiceName != null) {
            BundleContext ctx = m_component.getBundleContext();
            if (m_trackedServiceFilter != null) {
                try {
                    m_tracker = new ServiceTracker(ctx, ctx.createFilter(m_trackedServiceFilter), this);
                } catch (InvalidSyntaxException e) {
                    throw new IllegalStateException("Invalid filter definition for dependency: "
                        + m_trackedServiceFilter);
                }
            } else if (m_trackedServiceReference != null) {
                m_tracker = new ServiceTracker(ctx, m_trackedServiceReference, this);
            } else {
                m_tracker = new ServiceTracker(ctx, m_trackedServiceName.getName(), this);
            }
        } else {
            throw new IllegalStateException("Could not create tracker for dependency, no service name specified.");
        }
        if (m_debug) {
            m_tracker.setDebug(m_debugKey);
        }
        m_tracker.open();
        super.start();
	}
	
	@Override
	public void stop() {
	    m_tracker.close();
	    m_tracker = null;
	    super.stop();
	}

	@Override
	public Object addingService(@SuppressWarnings("rawtypes") ServiceReference reference) {
		try {
			ServiceEventImpl event = new ServiceEventImpl(m_component, reference, null);
			if (obtainServiceBeforeInjecting()) {
				Object service = event.getEvent(); // will dereference the service object.
				if (service == null) {
					// service concurrently removed, ignore
					return null;
				}
			}
		    return event;
		} catch (IllegalStateException e) {
		    // most likely our bundle is being stopped. Only log an exception if our component is enabled.
		    if (m_component.isActive()) {
                m_component.getLogger().warn("could not handle service dependency for component %s", e,
                    m_component.getComponentDeclaration().getClassName());
		    }
		    return null;		    
		}
	}

	@Override
	public void addedService(@SuppressWarnings("rawtypes") ServiceReference reference, Object event) {
		ServiceEventImpl evt = (ServiceEventImpl) event;
		if (m_debug) {
			System.out.println(m_debugKey + " addedService: ref=" + reference);
		}
        m_component.handleEvent(this, EventType.ADDED, evt);
	}

	@Override
	public void modifiedService(@SuppressWarnings("rawtypes") ServiceReference reference, Object event) {
		ServiceEventImpl evt = (ServiceEventImpl) event;
		m_component.handleEvent(this, EventType.CHANGED, evt);
	}

	@Override
	public void removedService(@SuppressWarnings("rawtypes") ServiceReference reference, Object event) {
		ServiceEventImpl evt = (ServiceEventImpl) event;
        m_component.handleEvent(this, EventType.REMOVED, evt);
	}
	
	@SuppressWarnings("rawtypes")
	@Override
	public void swappedService(final ServiceReference reference, final Object service, final ServiceReference newReference, final Object newService) {
		ServiceEventImpl evt = (ServiceEventImpl) service;
		ServiceEventImpl newEvt = (ServiceEventImpl) newService;
		
		if (obtainServiceBeforeInjecting()) {
			try {
				newEvt.getEvent();
			} catch (IllegalStateException e) {
				// most likely our bundle is being stopped. Only log an exception if our component is enabled.
				if (m_component.isActive()) {
					m_component.getLogger().warn("could not handle service dependency for component %s", e,
							m_component.getComponentDeclaration().getClassName());
				}
				return;
			}
		}
				
		if (m_swap != null) {
			// it will not trigger a state change, but the actual swap should be scheduled to prevent things
			// getting out of order.		    		    
		    // We delegate the swap handling to the ComponentImpl, which is the class responsible for state management.
		    // The ComponentImpl will first check if the component is in the proper state so the swap method can be invoked.		    
            m_component.handleEvent(this, EventType.SWAPPED, evt, newEvt);
		} else {
			addedService(newReference, newService);
			removedService(reference, service);
		}
	}		
	
    @Override
    public void invokeCallback(EventType type, Event ... events) {
        switch (type) {
        case ADDED:
            if (m_add != null) {
                invoke (m_add, events[0], getInstances());
            }
            break;
        case CHANGED:
            if (m_change != null) {
                invoke (m_change, events[0], getInstances());
            }
            break;
        case REMOVED:
            if (m_remove != null) {
                invoke (m_remove, events[0], getInstances());
            }
            break;
        case SWAPPED:
            if (m_swap != null) {
                ServiceEventImpl oldEvent = (ServiceEventImpl) events[0];
                ServiceEventImpl newEvent = (ServiceEventImpl) events[1];
                invokeSwap(m_swap, oldEvent, newEvent, getInstances());
            }
            break;
        }
    }

	@Override
    public Class<?> getAutoConfigType() {
        return m_trackedServiceName;
    }
	
    @Override
    public DependencyContext createCopy() {
        return new ServiceDependencyImpl(this);
    }
    
    @Override
    public String getName() {
        StringBuilder sb = new StringBuilder();
        if (m_trackedServiceName != null) {
            sb.append(m_trackedServiceName.getName());
            if (m_trackedServiceFilterUnmodified != null) {
                sb.append(' ');
                sb.append(m_trackedServiceFilterUnmodified);
            }
        }
        if (m_trackedServiceReference != null) {
            sb.append("{service.id=" + m_trackedServiceReference.getProperty(Constants.SERVICE_ID) + "}");
        }
        return sb.toString();
    }
    
    @Override
    public String getSimpleName() {
        if (m_trackedServiceName != null) {
            return m_trackedServiceName.getName();
        }
        return null;
    }

    @Override
    public String getFilter() {
        if (m_trackedServiceFilterUnmodified != null) {
            return m_trackedServiceFilterUnmodified;
        } else if (m_trackedServiceReference != null) {
            return new StringBuilder("(").append(Constants.SERVICE_ID).append("=").append(
                String.valueOf(m_trackedServiceReferenceId)).append(")").toString();
        } else {
            return null;
        }
    }
    
    @Override
    public String getType() {
        return "service";
    }

	@SuppressWarnings("unchecked")
    @Override
    public Dictionary<String, Object> getProperties() {
        ServiceEventImpl se = (ServiceEventImpl) m_component.getDependencyEvent(this);
        if (se != null) {
            if (m_propagateCallbackInstance != null && m_propagateCallbackMethod != null) {
                try {
                    CallbackTypeDef callbackInfo = new CallbackTypeDef(new Class[][] { { ServiceReference.class, Object.class }, { ServiceReference.class } },
                        new Object[][] { { se.getReference(), se.getEvent() }, { se.getReference() } });
                    return (Dictionary<String, Object>) InvocationUtil.invokeCallbackMethod(m_propagateCallbackInstance, m_propagateCallbackMethod, callbackInfo.m_sigs, callbackInfo.m_args);
                } catch (InvocationTargetException e) {
                    m_component.getLogger().warn("Exception while invoking callback method", e.getCause());
                } catch (Throwable e) {
                    m_component.getLogger().warn("Exception while trying to invoke callback method", e);
                }
                throw new IllegalStateException("Could not invoke callback");
            } else {
                Hashtable<String, Object> props = new Hashtable<>();
                String[] keys = se.getReference().getPropertyKeys();
                for (int i = 0; i < keys.length; i++) {
                    if (!(keys[i].equals(Constants.SERVICE_ID) || keys[i].equals(Constants.SERVICE_PID))) {
                        props.put(keys[i], se.getReference().getProperty(keys[i]));
                    }
                }
                return props;
            }
        } else {
            throw new IllegalStateException("cannot find service reference");
        }
    }	
        
    /** Internal method to set the name, service reference and/or filter. */
    private void setService(Class<?> serviceName, ServiceReference<?> serviceReference, String serviceFilter) {
        ensureNotActive();
        if (serviceName == null) {
            m_trackedServiceName = Object.class;
        }
        else {
            m_trackedServiceName = serviceName;
        }
        if (serviceFilter != null) {
            m_trackedServiceFilterUnmodified = serviceFilter;
            if (serviceName == null) {
                m_trackedServiceFilter = serviceFilter;
            }
            else {
            	m_trackedServiceFilter = "(&(" + Constants.OBJECTCLASS + "=" + serviceName.getName() + ")" + serviceFilter + ")";            			
            }
        }
        else {
            m_trackedServiceFilterUnmodified = null;
            m_trackedServiceFilter = null;
        }
        if (serviceReference != null) {
            m_trackedServiceReference = serviceReference;
            if (serviceFilter != null) {
                throw new IllegalArgumentException("Cannot specify both a filter and a service reference.");
            }
            m_trackedServiceReferenceId = (Long) m_trackedServiceReference.getProperty(Constants.SERVICE_ID);
        }
        else {
            m_trackedServiceReference = null;
        }
    }
    
    @Override
    public Object getDefaultService(boolean nullObject) {
        Object service = null;
        if (isAutoConfig()) {
            service = getDefaultImplementation();
            if (service == null && nullObject) {
                service = getNullObject();
            }
        }
        return service;
    }
        
    private Object getNullObject() {
        if (m_nullObject == null) {
            Class<?> trackedServiceName;
            trackedServiceName = m_trackedServiceName;
            try {
                m_nullObject = Proxy.newProxyInstance(trackedServiceName.getClassLoader(),
                    new Class[] { trackedServiceName }, new DefaultNullObject());
            }
            catch (Throwable err) {
                m_component.getLogger().err("Could not create null object for %s.", err, trackedServiceName);
            }
        }
        return m_nullObject;
    }

    private Object getDefaultImplementation() {
        if (m_defaultImplementation != null) {
            if (m_defaultImplementation instanceof Class) {
                try {
                    m_defaultImplementationInstance = ((Class<?>) m_defaultImplementation).newInstance();
                }
                catch (Throwable e) {
                    m_component.getLogger().err("Could not create default implementation instance of class %s.", e,
                        m_defaultImplementation);
                }
            }
            else {
                m_defaultImplementationInstance = m_defaultImplementation;
            }
        }
        return m_defaultImplementationInstance;
    }

    public void invoke(String method, Event e, Object[] instances) {
        ServiceEventImpl se = (ServiceEventImpl) e;
        // Be as lazy as possible, in case no properties or map has to be injected
        Supplier<Dictionary<?,?>> properties = () -> se.getProperties();
        Supplier<ServicePropertiesMap> propertiesMap = () -> new ServicePropertiesMap(se.getReference());
        
        m_component.invokeCallback(instances, method,
            new Class[][]{
                {Component.class, ServiceReference.class, m_trackedServiceName},
                {Component.class, ServiceReference.class, Object.class}, 
                {Component.class, ServiceReference.class},
                {Component.class, m_trackedServiceName}, 
                {Component.class, Object.class}, 
                {Component.class},
                {Component.class, Map.class, m_trackedServiceName},
                {ServiceReference.class, m_trackedServiceName},
                {ServiceReference.class, Object.class}, 
                {ServiceReference.class},
                {m_trackedServiceName}, 
                {m_trackedServiceName, Map.class}, 
                {Map.class, m_trackedServiceName}, 
                {m_trackedServiceName, Dictionary.class}, 
                {Dictionary.class, m_trackedServiceName}, 
                {Object.class},
                {ServiceObjects.class},
                {}},
            
            new Supplier[][]{
                new Supplier<?>[] {() -> m_component, () -> se.getReference(), () -> se.getEvent()},
                new Supplier<?>[] {() -> m_component, () -> se.getReference(), () -> se.getEvent()}, 
                new Supplier<?>[] {() -> m_component, () -> se.getReference()}, 
                new Supplier<?>[] {() -> m_component, () -> se.getEvent()},
                new Supplier<?>[] {() -> m_component, () -> se.getEvent()},
                new Supplier<?>[] {() -> m_component},
                new Supplier<?>[] {() -> m_component, () -> propertiesMap.get(), () -> se.getEvent()},
                new Supplier<?>[] {() -> se.getReference(), () -> se.getEvent()},
                new Supplier<?>[] {() -> se.getReference(), () -> se.getEvent()}, 
                new Supplier<?>[] {() -> se.getReference()}, 
                new Supplier<?>[] {() -> se.getEvent()}, 
                new Supplier<?>[] {() -> se.getEvent(), () -> propertiesMap.get()},
                new Supplier<?>[] {() -> propertiesMap.get(), () -> se.getEvent()},
                new Supplier<?>[] {() -> se.getEvent(), () -> properties.get()},
                new Supplier<?>[] {() -> properties.get(), () -> se.getEvent()},
                new Supplier<?>[] {() -> se.getEvent()},
                new Supplier<?>[] {() -> se.getServiceObjects()},
                {}},
            true // log if method is not found
        );
    }
        
    private void invokeSwap(String swapMethod, ServiceEventImpl previous, ServiceEventImpl current, Object[] instances) {
    	if (m_debug) {
    		System.out.println("invoke swap: " + swapMethod + " on component " + m_component + ", instances: " + Arrays.toString(instances) + " - " + ((ComponentDeclaration)m_component).getState());
    	}
    	try {
    		m_component.invokeCallback(instances, swapMethod,
				new Class[][]{
            		{m_trackedServiceName, m_trackedServiceName}, 
            		{Object.class, Object.class},
            		{ServiceReference.class, m_trackedServiceName, ServiceReference.class, m_trackedServiceName},
            		{ServiceReference.class, Object.class, ServiceReference.class, Object.class},
            		{Component.class, m_trackedServiceName, m_trackedServiceName}, 
            		{Component.class, Object.class, Object.class},
            		{Component.class, ServiceReference.class, m_trackedServiceName, ServiceReference.class, m_trackedServiceName},
            		{Component.class, ServiceReference.class, Object.class, ServiceReference.class, Object.class},
            		{ServiceReference.class, ServiceReference.class},
            		{Component.class, ServiceReference.class, ServiceReference.class},
            		{ServiceObjects.class, ServiceObjects.class},
            		{Component.class, ServiceObjects.class, ServiceObjects.class},
            	}, 
	            
	            new Supplier[][]{
            		new Supplier<?>[] {() -> previous.getEvent(), () -> current.getEvent()}, 
            		new Supplier<?>[] {() -> previous.getEvent(), () -> current.getEvent()}, 
            		new Supplier<?>[] {() -> previous.getReference(), () -> previous.getEvent(), () -> current.getReference(), () -> current.getEvent()}, 
            		new Supplier<?>[] {() -> previous.getReference(), () -> previous.getEvent(), () -> current.getReference(), () -> current.getEvent()}, 
            		new Supplier<?>[] {() -> m_component, () -> previous.getEvent(), () -> current.getEvent()}, 
            		new Supplier<?>[] {() -> m_component, () -> previous.getEvent(), () -> current.getEvent()}, 
            		new Supplier<?>[] {() -> m_component, () -> previous.getReference(), () -> previous.getEvent(), () -> current.getReference(), () -> current.getEvent()}, 
            		new Supplier<?>[] {() -> m_component, () -> previous.getReference(), () -> previous.getEvent(), () -> current.getReference(), () -> current.getEvent()}, 
            		new Supplier<?>[] {() -> previous.getReference(), () -> current.getReference()}, 
            		new Supplier<?>[] {() -> m_component, () -> previous.getReference(), () -> current.getReference()},
            		new Supplier<?>[] {() -> previous.getServiceObjects(), () -> current.getServiceObjects()}, 
            		new Supplier<?>[] {() -> m_component, () -> previous.getServiceObjects(), () -> current.getServiceObjects()}            		
            	},
            		
            	true); // log if not found
    	} catch (Throwable e) {
    	    m_component.getLogger().err("Could not invoke swap callback", e);
    	}
	}
    
    private boolean obtainServiceBeforeInjecting() {
    	return m_obtainServiceBeforeInjection && ! m_component.injectionDisabled();
    }
	
}
