/**
 * 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.aries.subsystem.modelling.impl;

import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

import org.apache.aries.blueprint.ComponentDefinitionRegistry;
import org.apache.aries.subsystem.modelling.ExportedService;
import org.apache.aries.subsystem.modelling.ImportedService;
import org.apache.aries.subsystem.modelling.InvalidAttributeException;
import org.apache.aries.subsystem.modelling.ModellingManager;
import org.apache.aries.subsystem.modelling.ParsedServiceElements;
import org.apache.aries.subsystem.modelling.ParserProxy;
import org.apache.aries.subsystem.modelling.WrappedServiceMetadata;
import org.apache.aries.util.manifest.ManifestHeaderProcessor;
import org.osgi.service.blueprint.reflect.BeanArgument;
import org.osgi.service.blueprint.reflect.BeanMetadata;
import org.osgi.service.blueprint.reflect.BeanProperty;
import org.osgi.service.blueprint.reflect.CollectionMetadata;
import org.osgi.service.blueprint.reflect.ComponentMetadata;
import org.osgi.service.blueprint.reflect.MapEntry;
import org.osgi.service.blueprint.reflect.MapMetadata;
import org.osgi.service.blueprint.reflect.Metadata;
import org.osgi.service.blueprint.reflect.RefMetadata;
import org.osgi.service.blueprint.reflect.ReferenceListMetadata;
import org.osgi.service.blueprint.reflect.ReferenceListener;
import org.osgi.service.blueprint.reflect.RegistrationListener;
import org.osgi.service.blueprint.reflect.ServiceMetadata;
import org.osgi.service.blueprint.reflect.ServiceReferenceMetadata;
import org.osgi.service.blueprint.reflect.Target;
import org.osgi.service.blueprint.reflect.ValueMetadata;
import org.osgi.service.jndi.JNDIConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

abstract public class AbstractParserProxy implements ParserProxy {
	  private final Logger _logger = LoggerFactory.getLogger(AbstractParserProxy.class);
	  private ModellingManager _modellingManager;


      protected abstract ComponentDefinitionRegistry parseCDR(List<URL> blueprintsToParse) throws Exception;
      protected abstract ComponentDefinitionRegistry parseCDR(InputStream blueprintToParse) throws Exception;
	  
	  public void setModellingManager (ModellingManager m) { 
	    _modellingManager = m;
	  }

	  public List<? extends WrappedServiceMetadata> parse(List<URL> blueprintsToParse) throws Exception {
	    _logger.debug("Method entry: {}, args {}", "parse", new Object[]{blueprintsToParse});
	    ComponentDefinitionRegistry cdr = parseCDR (blueprintsToParse);
	    List<? extends WrappedServiceMetadata> result = parseCDRForServices (cdr, true);
	    _logger.debug("Method exit: {}, returning {}", "parse", new Object[]{result});
	    return result;
	  }
	   
	  public List<? extends WrappedServiceMetadata> parse(URL blueprintToParse) throws Exception {
	    _logger.debug("Method entry: {}, args {}", "parse", new Object[]{blueprintToParse});
	    List<URL> list = new ArrayList<URL>();
	    list.add(blueprintToParse);
	   
	    List<? extends WrappedServiceMetadata> result = parse (list);
	    _logger.debug("Method exit: {}, returning {}", "parse", new Object[]{result});
	    return result;
	  }

	  public List<? extends WrappedServiceMetadata> parse(InputStream blueprintToParse) throws Exception {
	    _logger.debug("Method entry: {}, args {}", "parse", new Object[]{blueprintToParse});
	    ComponentDefinitionRegistry cdr = parseCDR (blueprintToParse);
	    List<? extends WrappedServiceMetadata> result = parseCDRForServices (cdr, true);
	    _logger.debug("Method exit: {}, returning {}", "parse", new Object[]{result});
	    return result;
	  }
	 

	  public ParsedServiceElements parseAllServiceElements(InputStream blueprintToParse) throws Exception {
	    _logger.debug("Method entry: {}, args {}", "parseAllServiceElements", new Object[]{blueprintToParse});
	    ComponentDefinitionRegistry cdr = parseCDR (blueprintToParse);
	    Collection<ExportedService> services = parseCDRForServices(cdr, false);
	    Collection<ImportedService> references = parseCDRForReferences (cdr);
	    ParsedServiceElements result = _modellingManager.getParsedServiceElements(services, references);
	    _logger.debug("Method exit: {}, returning {}", "parseAllServiceElements", new Object[]{result});
	    return result;
	  }
	  
	  /**
	   * Extract Service metadata from a ComponentDefinitionRegistry. When doing SCA modelling, we
	   * need to suppress anonymous services. We don't want to do that when we're modelling for 
	   * provisioning dependencies. 
	   * @param cdr                       ComponentDefinitionRegistry
	   * @param suppressAnonymousServices Unnamed services will not be returned if this is true
	   * @return List<WrappedServiceMetadata>
	   */
	  private List<ExportedService> parseCDRForServices (ComponentDefinitionRegistry cdr, 
	      boolean suppressAnonymousServices) { 
	    _logger.debug("Method entry: {}, args {}", "parseCDRForServices", new Object[]{cdr, suppressAnonymousServices});
	    List<ExportedService> result = new ArrayList<ExportedService>();
	    for (ComponentMetadata compMetadata : findAllComponents(cdr)) { 
	      if (compMetadata instanceof ServiceMetadata) { 
	        ServiceMetadata serviceMetadata = (ServiceMetadata)compMetadata;
	        String serviceName;
	        int ranking;
	        Collection<String> interfaces = new ArrayList<String>(); 
	        Map<String, Object> serviceProps = new HashMap<String, Object>();

	        ranking = serviceMetadata.getRanking();
	        for (Object i : serviceMetadata.getInterfaces()) {
	          interfaces.add((String)i);
	        }
	        
	        // get the service properties
	        List<MapEntry> props = serviceMetadata.getServiceProperties();
	        for (MapEntry entry : props) { 
	          String key = ((ValueMetadata)entry.getKey()).getStringValue();
	          
	          Metadata value = entry.getValue();
	          if (value instanceof CollectionMetadata) { 
	            processMultiValueProperty(serviceProps, key, value);
	          } else { 
	            serviceProps.put(key, ((ValueMetadata)entry.getValue()).getStringValue());
	          }
	        }

	        // serviceName: use the service id unless that's not set, 
	        // in which case we use the bean id. 
	        serviceName = serviceMetadata.getId();
	        
	        // If the Service references a Bean, export the bean id as a service property
	        // as per 121.6.5 p669 of the blueprint 1.0 specification
	        Target t = serviceMetadata.getServiceComponent();
	        String targetId = null;
	        if (t instanceof RefMetadata) { 
	          targetId = ((RefMetadata)t).getComponentId();
	        } else if (t instanceof BeanMetadata) { 
	          targetId = ((BeanMetadata)t).getId();
	        }
	        
	        // Our OBR code MUST have access to targetId if it's available (i.e. not null 
	        // or auto-generated for an anonymous service. This must ALWAYS be set. 
	        if (targetId != null && !targetId.startsWith(".")) { // Don't set this for anonymous inner components
	            serviceProps.put("osgi.service.blueprint.compname", targetId);
	          if (serviceName == null || serviceName.equals("") || serviceName.startsWith(".")) { 
	            serviceName = targetId;
	          }
	        }
	        
	        if(serviceName != null && serviceName.startsWith("."))
	          serviceName = null;
	        
	        // If suppressAnonymous services, do not expose services that have no name
	        if (!suppressAnonymousServices || (serviceName != null)) { 
	          ExportedService wsm = _modellingManager.getExportedService(serviceName, ranking, interfaces, serviceProps);
	          result.add(wsm);
	        }
	      }
	    }
	    _logger.debug("Method exit: {}, returning {}", "parseAllServiceElements", new Object[]{result});
	    return result; 
	  }
    private void processMultiValueProperty(Map<String, Object> serviceProps,
        String key, Metadata value) {
      List<Metadata> values = ((CollectionMetadata)value).getValues();
      Class<?> collectionClass = ((CollectionMetadata)value).getCollectionClass();
      Object collectionValue;
      
      if(Collection.class.isAssignableFrom(collectionClass)) {
        Collection<String> theseValues = getCollectionFromClass(collectionClass);
        for(Metadata m : values) {
          theseValues.add(((ValueMetadata)m).getStringValue());
        }
        collectionValue = theseValues;
      } else {
        String[] theseValues = new String[values.size()];
        for (int i=0; i < values.size(); i++) { 
          Metadata m = values.get(i); 
          theseValues[i] = ((ValueMetadata)m).getStringValue();
        }
        collectionValue = theseValues;
      }
      serviceProps.put(key, collectionValue);
    }	  
	  
	  private Collection<String> getCollectionFromClass(Class<?> collectionClass) {
	    
	    if(List.class.isAssignableFrom(collectionClass)) {
	      return new ArrayList<String>();
	    } else if (Set.class.isAssignableFrom(collectionClass)) {
	      return new LinkedHashSet<String>();
	    } else if (Queue.class.isAssignableFrom(collectionClass)) {
	      //This covers Queue and Deque, which is caught by the isAssignableFrom check
	      //as a sub-interface of Queue
	      return new LinkedList<String>();
	    } else {
	      throw new IllegalArgumentException(collectionClass.getName());
	    }
	  }
	  
    /**
	   * Extract References metadata from a ComponentDefinitionRegistry. 
	   * @param cdr                       ComponentDefinitionRegistry
	   * @return List<WrappedReferenceMetadata>
	   * @throws InvalidAttributeException 
	   */
	  private List<ImportedService> parseCDRForReferences (ComponentDefinitionRegistry cdr) throws InvalidAttributeException { 
	    _logger.debug("Method entry: {}, args {}", "parseCDRForReferences", new Object[]{cdr});
	    List<ImportedService> result = new ArrayList<ImportedService>();
	    for (ComponentMetadata compMetadata : findAllComponents(cdr)) { 
	      if (compMetadata instanceof ServiceReferenceMetadata) { 
	        ServiceReferenceMetadata referenceMetadata = (ServiceReferenceMetadata)compMetadata;

	        boolean optional = referenceMetadata.getAvailability() == ServiceReferenceMetadata.AVAILABILITY_OPTIONAL;
	        String iface = referenceMetadata.getInterface();
	        String compName = referenceMetadata.getComponentName();
	        String blueprintFilter = referenceMetadata.getFilter();
	        String id = referenceMetadata.getId();
	        boolean isMultiple = referenceMetadata instanceof ReferenceListMetadata;
	        
	        //The blueprint parser teams up with JPA and blueprint resource ref
	        // namespace handlers to give us service imports of the form, 
	        // objectClass=javax.persistence.EntityManagerFactory, org.apache.aries.jpa.proxy.factory=*, osgi.unit.name=blabber
	        //
	        // There will be no matching service for this reference. 
	        // For now we blacklist certain objectClasses and filters - this is a pretty dreadful thing to do. 
	        if (!isBlacklisted (iface, blueprintFilter)) { 
	          ImportedService ref = _modellingManager.getImportedService (optional, iface, compName, blueprintFilter, 
	              id, isMultiple);
	          result.add (ref);  
	        }
	      }
	    }
	    _logger.debug("Method exit: {}, returning {}", "parseCDRForReferences", new Object[]{result});
	    return result; 
	  }
	  
	  /**
	   * Find all the components in a given {@link ComponentDefinitionRegistry} this finds top-level
	   * components as well as their nested counter-parts. It may however not find components in custom namespacehandler 
	   * {@link ComponentMetadata} instances.
	   * 
	   * @param cdr The {@link ComponentDefinitionRegistry} to scan
	   * @return a {@link Set} of {@link ComponentMetadata}
	   */
	  private Set<ComponentMetadata> findAllComponents(ComponentDefinitionRegistry cdr) {
	      Set<ComponentMetadata> components = new HashSet<ComponentMetadata>();
	      
	      for (String name : cdr.getComponentDefinitionNames()) {
	          ComponentMetadata component = cdr.getComponentDefinition(name);
	          traverseComponent(component, components);
	      }
	      
	      return components;
	  }
	  
	  /**
	   * Traverse to find all nested {@link ComponentMetadata} instances
	   * @param metadata
	   * @param output
	   */
	  private void traverse(Metadata metadata, Set<ComponentMetadata> output) {
	      if (metadata instanceof ComponentMetadata) {
	          traverseComponent((ComponentMetadata) metadata, output);	          
	      } else if (metadata instanceof CollectionMetadata) {
	          CollectionMetadata collection = (CollectionMetadata) metadata;
	          
	          for (Metadata v : collection.getValues()) traverse(v, output);
	      } else if (metadata instanceof MapMetadata) {
	          MapMetadata map = (MapMetadata) metadata;
	          
	          for (MapEntry e : map.getEntries()) {
	              traverse(e.getKey(), output);
	              traverse(e.getValue(), output);
	          }
	      }
	  }
	  
	  /**
	   * Traverse {@link ComponentMetadata} instances to find all nested {@link ComponentMetadata} instances
	   * @param component
	   * @param output
	   */
	  private void traverseComponent(ComponentMetadata component, Set<ComponentMetadata> output) {
	      if (!output.add(component)) return;
	      
	      if (component instanceof BeanMetadata) {
	          BeanMetadata bean = (BeanMetadata) component;
	          
	          traverse(bean.getFactoryComponent(), output);

	          for (BeanArgument argument : bean.getArguments()) {
	              traverse(argument.getValue(), output);
	          }
	          
	          for (BeanProperty property : bean.getProperties()) {
	              traverse(property.getValue(), output);
	          }
	          
	      } else if (component instanceof ServiceMetadata) {
	          ServiceMetadata service = (ServiceMetadata) component;
	          	          
	          traverse(service.getServiceComponent(), output);
	          
	          for (RegistrationListener listener : service.getRegistrationListeners()) {
	               traverse(listener.getListenerComponent(), output);
	          }
	          
	          for (MapEntry e : service.getServiceProperties()) {
	              traverse(e.getKey(), output);
	              traverse(e.getValue(), output);
	          }
	          
	      } else if (component instanceof ServiceReferenceMetadata) {
	          ServiceReferenceMetadata reference = (ServiceReferenceMetadata) component;
	          
	          for (ReferenceListener listener : reference.getReferenceListeners()) {
	              traverse(listener.getListenerComponent(), output);
	          }
	      }
	  }
	
	  /**
	   * Some services are injected directly into isolated frameworks by default. We do 
	   * not need to model these services. They are not represented as ExportedServices 
	   * (Capabilities) in the various OBR registries, and so cannot be resolved against. 
	   * Since they are injected directly into each isolated framework, we do not need
	   * an entry in DEPLOYMENT.MF's Deployed-ImportService header for any of these 
	   * services. 
	   * 
	   * @param iface           The interface declared on a blueprint reference
	   * @param blueprintFilter The filter on the blueprint reference
	   * @return                True if the service is not 'blacklisted' and so may be exposed
	   *                        in the model being generated. 
	   */
	  protected boolean isBlacklisted (String iface, String blueprintFilter) { 
	    _logger.debug("Method entry: {}, args {}", "isBlacklisted", new Object[]{iface, blueprintFilter});
	    boolean blacklisted = false;
	    if (iface != null) {
	      // JPA - detect interface;
	      blacklisted |= iface.equals("javax.persistence.EntityManagerFactory");
	      blacklisted |= iface.equals("javax.persistence.EntityManager");
	    
	      // JTA - detect interface
	      blacklisted |= iface.equals("javax.transaction.UserTransaction");
	      blacklisted |= iface.equals("javax.transaction.TransactionSynchronizationRegistry");
	      
	      // ConfigurationAdmin - detect interface
	      blacklisted |= iface.equals("org.osgi.service.cm.ConfigurationAdmin");
	               
	      // Don't provision against JNDI references
	      if (blueprintFilter != null && blueprintFilter.trim().length() != 0) { 
	        Map<String, String> filter = ManifestHeaderProcessor.parseFilter(blueprintFilter);
	        blacklisted |= filter.containsKey(JNDIConstants.JNDI_SERVICENAME);
	      }
	    }
	    _logger.debug("Method exit: {}, returning {}", "isBlacklisted", new Object[]{!blacklisted});
	    return blacklisted;
	  }
}
