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

import static org.apache.felix.dm.lambda.impl.ComponentBuilderImpl.ComponentCallback.DESTROY;
import static org.apache.felix.dm.lambda.impl.ComponentBuilderImpl.ComponentCallback.INIT;
import static org.apache.felix.dm.lambda.impl.ComponentBuilderImpl.ComponentCallback.START;
import static org.apache.felix.dm.lambda.impl.ComponentBuilderImpl.ComponentCallback.STOP;

import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;

import org.apache.felix.dm.Component;
import org.apache.felix.dm.Component.ServiceScope;
import org.apache.felix.dm.ComponentStateListener;
import org.apache.felix.dm.Dependency;
import org.apache.felix.dm.DependencyManager;
import org.apache.felix.dm.context.ComponentContext;
import org.apache.felix.dm.lambda.BundleDependencyBuilder;
import org.apache.felix.dm.lambda.ComponentBuilder;
import org.apache.felix.dm.lambda.ConfigurationDependencyBuilder;
import org.apache.felix.dm.lambda.DependencyBuilder;
import org.apache.felix.dm.lambda.FluentProperty;
import org.apache.felix.dm.lambda.FutureDependencyBuilder;
import org.apache.felix.dm.lambda.ServiceDependencyBuilder;
import org.apache.felix.dm.lambda.callbacks.InstanceCb;
import org.apache.felix.dm.lambda.callbacks.InstanceCbComponent;

public class ComponentBuilderImpl implements ComponentBuilder<ComponentBuilderImpl> {
    private final List<DependencyBuilder<?>> m_dependencyBuilders = new ArrayList<>();
    private final List<Dependency> m_dependencies = new ArrayList<>();
    private final Component m_component;
    private final boolean m_componentUpdated;
    private String[] m_serviceNames;
    private Dictionary<Object, Object> m_properties;
    private Object m_impl;
    private Object m_factory;
    private boolean m_factoryHasComposite;	
    private boolean m_autoAdd = true;
    protected final Map<ComponentCallback, MethodRef> m_refs = new HashMap<>();
    private Object m_compositionInstance;
    private String m_compositionMethod;
    private String m_init;
    private String m_start;
    private String m_stop;
    private String m_destroy;
    private String m_factoryCreateMethod;
    private boolean m_hasFactoryRef;   
    private boolean m_hasFactory;
    private Object m_initCallbackInstance;
    private Object m_startCallbackInstance;
    private Object m_stopCallbackInstance;
    private Object m_destroyCallbackInstance;
    private final List<ComponentStateListener> m_listeners = new ArrayList<>();
    
    enum ComponentCallback { INIT, START, STOP, DESTROY };
    
    @FunctionalInterface
    interface MethodRef {
        public void accept(Component c);
    }

    public ComponentBuilderImpl(DependencyManager dm) {
        m_component = dm.createComponent();
        m_componentUpdated = false;
    }
    
    public ComponentBuilderImpl(Component component, boolean update) {
        m_component = component;
        m_componentUpdated = update;
    }
    
    @Override
    public ComponentBuilderImpl scope(ServiceScope scope) {
    	m_component.setScope(scope);
    	return this;
    }
        
    @Override
    public ComponentBuilderImpl autoConfig(Class<?> clazz, boolean autoConfig) {
        m_component.setAutoConfig(clazz, autoConfig);
        return this;
    }

    @Override
    public ComponentBuilderImpl autoConfig(Class<?> clazz, String instanceName) {
        m_component.setAutoConfig(clazz, instanceName);
        return this;
    }

    @Override
    public ComponentBuilderImpl provides(Class<?> iface) {
        m_serviceNames = new String[] {iface.getName()};
        return this;
    }

    @Override
    public ComponentBuilderImpl provides(Class<?>  iface, String name, Object value, Object ... rest) {
        provides(iface);
        properties(name, value, rest);
        return this;
    }
    
    @Override
    public ComponentBuilderImpl provides(Class<?>  iface, FluentProperty ... properties) {
        provides(iface);
        properties(properties);
        return this;
    }
    
    @Override
    public ComponentBuilderImpl provides(Class<?>  iface, Dictionary<?,?> properties) {
        provides(iface);
        properties(properties);
        return this;
    }

    @Override
    public ComponentBuilderImpl  provides(Class<?>[] ifaces) {
        m_serviceNames = Stream.of(ifaces).map(c -> c.getName()).toArray(String[]::new);
        return this;
    }
    
    @Override
    public ComponentBuilderImpl provides(Class<?>[] ifaces, String name, Object value, Object ... rest) {
        provides(ifaces);
        properties(name, value, rest);
        return this;
    }
    
    @Override
    public ComponentBuilderImpl provides(Class<?>[] ifaces, FluentProperty ... properties) {
        provides(ifaces);
        properties(properties);
        return this;
    }
    
    @Override
    public ComponentBuilderImpl provides(Class<?>[] ifaces, Dictionary<?,?> properties) {
        provides(ifaces);
        properties(properties);
        return this;
    }

    @Override
    public ComponentBuilderImpl provides(String iface) {
        m_serviceNames = new String[] {iface};
        return this;
    }

    @Override
    public ComponentBuilderImpl provides(String iface, String name, Object value, Object ... rest) {
        provides(iface);
        properties(name, value, rest);
        return this;
    }
    
    @Override
    public ComponentBuilderImpl provides(String iface, FluentProperty ... properties) {
        provides(iface);
        properties(properties);
        return this;
    }
    
    @Override
    public ComponentBuilderImpl provides(String  iface, Dictionary<?,?> properties) {
        provides(iface);
        properties(properties);
        return this;
    }

    @Override
    public ComponentBuilderImpl  provides(String[] ifaces) {
        m_serviceNames = ifaces;
        return this;
    }
    
    @Override
    public ComponentBuilderImpl provides(String[] ifaces, String name, Object value, Object ... rest) {
        provides(ifaces);
        properties(name, value, rest);
        return this;
    }
    
    @Override
    public ComponentBuilderImpl provides(String[] ifaces, FluentProperty ... properties) {
        provides(ifaces);
        properties(properties);
        return this;
    }
    
    @Override
    public ComponentBuilderImpl provides(String[] ifaces, Dictionary<?,?> properties) {
        provides(ifaces);
        properties(properties);
        return this;
    }    
    
    @SuppressWarnings("unchecked")
    @Override
    public ComponentBuilderImpl properties(Dictionary<?, ?> properties) {
        m_properties = (Dictionary<Object, Object>) properties;
        return this;
    }

    @Override
    public ComponentBuilderImpl properties(String name, Object value, Object ... rest) {
    	Objects.nonNull(name);
    	Objects.nonNull(value);
        Properties props = new Properties();
        props.put(name,  value);
        if ((rest.length & 1) != 0) {
        	throw new IllegalArgumentException("Invalid number of specified properties (number of arguments must be even).");
        }
        for (int i = 0; i < rest.length - 1; i += 2) {
            String k = rest[i].toString().trim();
            Object v = rest[i+1];
            props.put(k, v);
        }
        m_properties = props;
        return this;
    }
    
    @Override
    public ComponentBuilderImpl properties(FluentProperty ... properties) {
    	Dictionary<Object, Object> props = new Hashtable<>();
    	Stream.of(properties).forEach(property -> {
    		String name = Helpers.getLambdaParameterName(property, 0);
    		if (name.equals("arg0")) {
    			throw new IllegalArgumentException("arg0 property name not supported"); 
    		}
    		Object value = property.apply(name);
    		props.put(convertDots(name), value);
    	});
        m_properties = props;
        return this;
    }

    @Override
    public ComponentBuilderImpl debug(String label) {
        m_component.setDebug(label);
        return this;
    }
    
    @Override
    public ComponentBuilderImpl autoAdd(boolean autoAdd) {
        m_autoAdd = autoAdd;
        return this;
    }
    
    public ComponentBuilderImpl autoAdd() {
        m_autoAdd = true;
    	return this;
    }

    public boolean isAutoAdd() {
        return m_autoAdd;
    }

    @Override
    public ComponentBuilderImpl impl(Object instance) {
        m_impl = instance;
        return this;
    }
        
    @Override
    public ComponentBuilderImpl factory(Object factory, String createMethod) {
        m_factory = factory;
        m_factoryCreateMethod = createMethod;
        ensureHasNoFactoryRef();
        m_hasFactory = true;
        return this;
    }

    @Override
    public ComponentBuilderImpl factory(Supplier<?> create) {
        Objects.nonNull(create);
        ensureHasNoFactory();
        m_hasFactoryRef = true;
        m_factory = new Object() {
            @SuppressWarnings("unused")
            public Object create() {
                return create.get();
            }
            @Override
            public String toString() {
                return create.getClass().getName() + " (Factory)";
            }
        };
        return this;
    }
    
    @Override
    public <U, V> ComponentBuilderImpl factory(Supplier<U> supplier, Function<U, V> create) {
        Objects.nonNull(supplier);
        Objects.nonNull(create);
        ensureHasNoFactory();
        m_hasFactoryRef = true;

        m_factory = new Object() {
            @SuppressWarnings("unused")
            public Object create() {
                U factoryImpl = supplier.get();
                return create.apply(factoryImpl);
            }
            @Override
            public String toString() {
                return supplier.getClass().getName() + " (Factory)";
            }
        }; 
        return this;
    }

    @Override
    public ComponentBuilderImpl factory(Supplier<?> create, Supplier<Object[]> getComposite) {
        Objects.nonNull(create);
        Objects.nonNull(getComposite);
        ensureHasNoFactory();
        m_hasFactoryRef = true;

        m_factory = new Object() {
            @SuppressWarnings("unused")
            public Object create() { // Create Factory instance
                return create.get();
            }
            
            @SuppressWarnings("unused")
            public Object[] getComposite() { // Create Factory instance
                return getComposite.get();
            }
            
            @Override
            public String toString() {
                return create.getClass().getName() + " (Factory)";
            }
        };
        m_factoryHasComposite = true;
        return this;
    }
    
    @Override
    public <U> ComponentBuilderImpl factory(Supplier<U> factorySupplier, Function<U, ?> factoryCreate, Function<U, Object[]> factoryGetComposite) {
        Objects.nonNull(factorySupplier);
        Objects.nonNull(factoryCreate);
        Objects.nonNull(factoryGetComposite);
        ensureHasNoFactory();
        m_hasFactoryRef = true;

        m_factory = new Object() {
            U m_factoryInstance;
            
            @SuppressWarnings("unused")
            public Object create() { 
                m_factoryInstance = factorySupplier.get();
                return factoryCreate.apply(m_factoryInstance);
            }
            
            @SuppressWarnings("unused")
            public Object[] getComposite() { 
                return factoryGetComposite.apply(m_factoryInstance);
            }
            
            @Override
            public String toString() {
                return factorySupplier.getClass().getName() + " (Factory)";
            }
        }; 
        m_factoryHasComposite = true;
        return this;
    }
        
    public ComponentBuilderImpl composition(String getCompositionMethod) {
        return composition(null, getCompositionMethod);
    }
    
    public ComponentBuilderImpl composition(Object instance, String getCompositionMethod) {
        m_compositionInstance = instance;
        m_compositionMethod = getCompositionMethod;
        return this;
    }
    
    public ComponentBuilderImpl composition(Supplier<Object[]> getCompositionMethod) {
        m_compositionInstance = new Object() {
            @SuppressWarnings("unused")
            public Object[] getComposition() {
                return getCompositionMethod.get();
            }
        };
        m_compositionMethod = "getComposition";
        return this;
    }
    
    @Override
    public ComponentBuilderImpl withDep(Dependency dep) {
    	m_dependencies.add(dep);
    	return this;
    }
    
   @Override
    public <U> ComponentBuilderImpl withSvc(Class<U> service, Consumer<ServiceDependencyBuilder<U>> consumer) {
        ServiceDependencyBuilder<U> dep = new ServiceDependencyBuilderImpl<>(m_component, service);
        consumer.accept(dep);
        m_dependencyBuilders.add(dep);
        return this;
    }   
        
    @Override
    public ComponentBuilderImpl withCnf(Consumer<ConfigurationDependencyBuilder> consumer) {
        ConfigurationDependencyBuilder dep = new ConfigurationDependencyBuilderImpl(m_component);
        consumer.accept(dep);
        m_dependencyBuilders.add(dep);
        return this;
    }
    
    @Override
    public ComponentBuilderImpl withBundle(Consumer<BundleDependencyBuilder> consumer) {
    	BundleDependencyBuilder dep = new BundleDependencyBuilderImpl(m_component);
        consumer.accept(dep);
        m_dependencyBuilders.add(dep);
        return this;
    }
           
    @Override
    public <V> ComponentBuilderImpl withFuture(CompletableFuture<V> future, Consumer<FutureDependencyBuilder<V>> consumer) {
        FutureDependencyBuilder<V> dep = new CompletableFutureDependencyImpl<>(m_component, future);
        consumer.accept(dep);
        m_dependencyBuilders.add(dep);
        return this;
    }
    
    public ComponentBuilderImpl init(String callback) {
        m_init = callback;
        return this;
    }
    
    public ComponentBuilderImpl init(Object callbackInstance, String callback) {
        init(callback);
        m_initCallbackInstance = callbackInstance;
        return this;
    }

    @Override
    public ComponentBuilderImpl init(InstanceCb callback) {
        setCallbackMethodRef(INIT, component -> callback.callback());
        m_init = null;
        m_initCallbackInstance = null;
        return this;
    }
    
    @Override
    public ComponentBuilderImpl init(InstanceCbComponent callback) {
        setCallbackMethodRef(INIT, component -> callback.accept(component));
        m_init = null;
        m_initCallbackInstance = null;
        return this;
    }
  
    public ComponentBuilderImpl start(String callback) {
        m_start = callback;
        return this;
    }
    
    public ComponentBuilderImpl start(Object callbackInstance, String callback) {
        start(callback);
        m_startCallbackInstance = callbackInstance;
        return this;
    }

    @Override
    public ComponentBuilderImpl start(InstanceCb callback) {
        setCallbackMethodRef(START, component -> callback.callback());
        m_start = null;
        m_startCallbackInstance = null;
        return this;
    }
    
    @Override
    public ComponentBuilderImpl start(InstanceCbComponent callback) {
        setCallbackMethodRef(START, component -> callback.accept(component));
        m_start = null;
        m_startCallbackInstance = null;
        return this;
    }

    public ComponentBuilderImpl stop(String callback) {
        m_stop = callback;
        return this;
    }
    
    public ComponentBuilderImpl stop(Object callbackInstance, String callback) {
        stop(callback);
        m_stopCallbackInstance = callbackInstance;
        return this;
    }

    @Override
    public ComponentBuilderImpl stop(InstanceCb callback) {
        setCallbackMethodRef(STOP, component -> callback.callback());
        m_stop = null;
        m_stopCallbackInstance = null;
        return this;
    }

    @Override
    public ComponentBuilderImpl stop(InstanceCbComponent callback) {
        setCallbackMethodRef(STOP, component -> callback.accept(component));
        m_stop = null;
        m_stopCallbackInstance = null;
        return this;
    }

    public ComponentBuilderImpl destroy(String callback) {
        m_destroy = callback;
        return this;
    }
           
    public ComponentBuilderImpl destroy(Object callbackInstance, String callback) {
        destroy(callback);
        m_destroyCallbackInstance = callbackInstance;
        return this;
    }

    @Override
    public ComponentBuilderImpl destroy(InstanceCb callback) {
        setCallbackMethodRef(DESTROY, component -> callback.callback());
        m_destroy = null;
        m_destroyCallbackInstance = null;
        return this;
    }
        
    @Override
    public ComponentBuilderImpl destroy(InstanceCbComponent callback) {
        setCallbackMethodRef(DESTROY, component -> callback.accept(component));
        m_destroy = null;
        m_destroyCallbackInstance = null;
        return this;
    }
        
    public ComponentBuilderImpl listener(ComponentStateListener listener) {
    	m_listeners.add(listener);
    	return this;
    }
    
    public Component build() {
        if (m_serviceNames != null) {
            m_component.setInterface(m_serviceNames, m_properties);
        } 
        
        if (m_properties != null) {
            m_component.setServiceProperties(m_properties);
        }
        
        m_listeners.stream().forEach(m_component::add);

        if (! m_componentUpdated) { // Don't override impl or set callbacks if component is being updated
           if (m_impl != null) {               
               m_component.setImplementation(m_impl);
               m_component.setComposition(m_compositionInstance, m_compositionMethod);
           } else {
               Objects.nonNull(m_factory);
               if (m_hasFactoryRef) {
                   m_component.setFactory(m_factory, "create");
                   if (m_factoryHasComposite) {
                       m_component.setComposition(m_factory, "getComposite");
                   }
               } else {
                   m_component.setFactory(m_factory, m_factoryCreateMethod);
               }
           }                       
           
           if (hasCallbacks()) { // either method refs on some object instances, or a callback (reflection) on some object instances.   
               if (m_refs.get(INIT) == null) {
                   setCallbackMethodRef(INIT, component -> invokeCallbacks(component, m_initCallbackInstance, m_init, "init"));
               }
               if (m_refs.get(START) == null) {
                   setCallbackMethodRef(START, component -> invokeCallbacks(component, m_startCallbackInstance, m_start, "start"));
               }
               if (m_refs.get(STOP) == null) {
                   setCallbackMethodRef(STOP, component -> invokeCallbacks(component, m_stopCallbackInstance, m_stop, "stop"));
               }
               if (m_refs.get(DESTROY) == null) {
                   setCallbackMethodRef(DESTROY, component -> invokeCallbacks(component, m_destroyCallbackInstance, m_destroy, "destroy"));
               }
               setInternalCallbacks();                
            }
        }
        
        if (m_dependencyBuilders.size() > 0) {
            // add atomically in case we are building some component dependencies from a component init method.
        	List<Dependency> depList = new ArrayList<>();
            m_dependencyBuilders.stream().map(builder -> builder.build()).forEach(depList::add);
            depList.addAll(m_dependencies);
            m_component.add(depList.stream().toArray(Dependency[]::new));
        }
        return m_component;
    }
        
    private boolean hasCallbacks() {
        return m_refs.size() > 0 || m_init != null || m_start != null || m_stop != null || m_destroy != null;
    }

    private void invokeCallbacks(Component component, Object callbackInstance, String callback, String defaultCallback) {
        boolean logIfNotFound = (callback != null);
        callback = callback != null ? callback : defaultCallback;
        ComponentContext ctx = (ComponentContext) component;
        Object[] instances = callbackInstance != null ? new Object[] { callbackInstance } : ctx.getInstances();

        ctx.invokeCallbackMethod(instances, callback, 
            new Class[][] {{ Component.class }, {}}, 
            new Object[][] {{ component }, {}},
            logIfNotFound);
    }
    
    private ComponentBuilderImpl setCallbackMethodRef(ComponentCallback cbType, MethodRef ref) {
        m_refs.put(cbType,  ref);
        return this;
    }
            
    @SuppressWarnings("unused")
    private void setInternalCallbacks() {
        Object cb = new Object() {
            void init(Component comp) {
                invokeLifecycleCallback(ComponentCallback.INIT, comp);
            }

            void start(Component comp) {
                invokeLifecycleCallback(ComponentCallback.START, comp);
            }

            void stop(Component comp) {
                invokeLifecycleCallback(ComponentCallback.STOP, comp);
            }

            void destroy(Component comp) {
                invokeLifecycleCallback(ComponentCallback.DESTROY, comp);
            }
        };
        m_component.setCallbacks(cb, "init", "start", "stop", "destroy");
    }
    
    private void invokeLifecycleCallback(ComponentCallback cbType, Component component) {        
        m_refs.computeIfPresent(cbType, (k, mref) -> {
            mref.accept(component);
            return mref;
         });
    }

    private void ensureHasNoFactoryRef() {
        if (m_hasFactoryRef) {
            throw new IllegalStateException("Can't mix factory method name and factory method reference");
        }
    }
    
    private void ensureHasNoFactory() {
        if (m_hasFactory) {
            throw new IllegalStateException("Can't mix factory method name and factory method reference");
        }
    }
    
    private String convertDots(String propertyName) {
        StringBuilder sb = new StringBuilder(propertyName);
        // replace "__" by "_" or "_" by ".": foo_bar -> foo.bar; foo__BAR_zoo -> foo_BAR.zoo
        for (int i = 0; i < sb.length(); i ++) {
            if (sb.charAt(i) == '_') {
                if (i < (sb.length() - 1) && sb.charAt(i+1) == '_') {
                    // replace foo__bar -> foo_bar
                    sb.replace(i, i+2, "_");
                } else {
                    // replace foo_bar -> foo.bar
                    sb.replace(i, i+1, ".");
                }
            }
        }
        return sb.toString();
    }
}
