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

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.jar.Attributes;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

import org.apache.aries.unittest.mocks.Skeleton;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceFactory;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;

import junit.framework.AssertionFailedError;

/**
 *
 */
/**
 * This class is a partial implementation of BundleContext. Its main function
 * is to provide a service registry implementation
 */
public class BundleContextMock
{
  /** The service registry */
  private static Map<String, List<ServiceData>> registry = new HashMap<String, List<ServiceData>>();
  /** A list of bundles installed into the runtime */
  private static List<Bundle> bundles = new ArrayList<Bundle>();
  /** A list of service listeners */
  private static List<ServiceListener> listeners = new ArrayList<ServiceListener>();
  /** The next service id to be assigned */
  private static long nextId = 0;

  private static class MockServiceFactory implements ServiceFactory
  {
    private final Object service;
    
    public MockServiceFactory(Object obj)
    {
      service = obj;
    }
    
    public Object getService(Bundle arg0, ServiceRegistration arg1)
    {
      return service;
    }

    public void ungetService(Bundle arg0, ServiceRegistration arg1, Object arg2)
    {
    }
  }
  
  private static class FilteredServiceListener implements ServiceListener
  {
    private Filter filter;
    private final ServiceListener listener;
    
    public FilteredServiceListener(String f, ServiceListener l)
    {
      listener = l;
      
      if (f != null) {
        try {
          filter = FrameworkUtil.createFilter(f);
        } catch (InvalidSyntaxException e) {
          AssertionFailedError err = new AssertionFailedError("The filter " + f + " is invalid");
          err.initCause(e);
          
          throw err;
        }
      }
    }

    public void serviceChanged(ServiceEvent arg0)
    {
      if (matches(arg0)) listener.serviceChanged(arg0);
    }

    private boolean matches(ServiceEvent arg0)
    {
      if (filter == null) return true;
      
      ServiceReference ref = arg0.getServiceReference();
      
      if (Skeleton.isSkeleton(ref)) {
        Object template = Skeleton.getSkeleton(ref).getTemplateObject();
        
        if (template instanceof ServiceData) {
          return filter.match(((ServiceData)template).getProperties());
        }
      }
      
      return filter.match(ref);
    }
    
    @Override
    public boolean equals(Object obj)
    {
      if (obj == null) return false;
      else if (obj instanceof FilteredServiceListener) {
        return listener.equals(((FilteredServiceListener)obj).listener);
      }
      
      return false;
    }
    
    @Override
    public int hashCode()
    {
      return listener.hashCode();
    }
  }
  
  /**
   * This class represents the information registered about a service. It also
   * implements part of the ServiceRegistration and ServiceReference interfaces.
   */
  private class ServiceData implements Comparable<ServiceReference>
  {
    /** The service that was registered */
    private ServiceFactory serviceImpl;
    /** the service properties */
    @SuppressWarnings("unused")
    private final Hashtable<String, Object> serviceProps = new Hashtable<String, Object>();
    /** The interfaces the service publishes with */
    private String[] interfaceNames;
    /** The bundle that defines this service */
    private Bundle registeringBundle;

    /**
     * This method unregisters the service from the registry.
     */
    public void unregister()
    {
      for (String interfaceName : interfaceNames) {
        List<ServiceData> list = registry.get(interfaceName);
        if (list != null) {
          list.remove(this);
          if (list.isEmpty()) {
            registry.remove(interfaceName);
          }
        }
      }
      notifyAllListeners(ServiceEvent.UNREGISTERING);
      registeringBundle = null;
    }

    /**
     * This method is used to register the service data in the registry
     */
    public void register()
    {
      for (String interfaceName : interfaceNames) {
        List<ServiceData> list = registry.get(interfaceName);
        if (list == null) {
          list = new ArrayList<ServiceData>();
          registry.put(interfaceName, list);
        }
        list.add(this);
      }
      notifyAllListeners(ServiceEvent.REGISTERED);
    }
    
    private void notifyAllListeners(int eventType) {
      List<ServiceListener> copy = new ArrayList<ServiceListener>(listeners.size());
      copy.addAll(listeners);
      for(ServiceListener listener : copy) {
        listener.serviceChanged(new ServiceEvent(eventType, Skeleton.newMock(this, ServiceReference.class)));
      }
    }
    
    /**
     * Change the service properties
     * @param newProps
     */
    public void setProperties(Dictionary<String,Object> newProps)
    {
      // make sure we don't overwrite framework properties
      newProps.put(Constants.OBJECTCLASS, serviceProps.get(Constants.OBJECTCLASS));
      newProps.put(Constants.SERVICE_ID, serviceProps.get(Constants.SERVICE_ID));

      Enumeration<String> keys = newProps.keys();
      
      serviceProps.clear();
      while (keys.hasMoreElements()) {
        String key = keys.nextElement();
        serviceProps.put(key, newProps.get(key));
      }
      
      notifyAllListeners(ServiceEvent.MODIFIED);
    }
    
    /**
     * This implements the isAssignableTo method from ServiceReference.
     * 
     * @param b
     * @param className
     * @return true if the referenced service can be assigned to the requested
     *              class name.
     */
    public boolean isAssignableTo(Bundle b, String className)
    {
      boolean result = false;
      
      for (String iName : interfaceNames)
      {
        result = iName.equals(className);
        
        if (result) break;
      }
      
      return result;
    }
    
    /**
     * Returns the requested service property.
     * @param key the property to return.
     * @return the property value.
     */
    public Object getProperty(String key)
    {
      return serviceProps.get(key);
    }
    
    @Override
    public boolean equals(Object o) {
      if(o == null) return false;
      
      if(o == this) return true;
      
      if (o instanceof ServiceData) {
        ServiceData other = (ServiceData) o;
        return serviceImpl == other.serviceImpl;
      }
      
      return false;
    }
    
    @Override
    public int hashCode()
    {
      return serviceImpl.hashCode();
    }
    
    /**
     * @return the keys of all the service properties.
     */
    public String[] getPropertyKeys()
    {
      Enumeration<String> e = serviceProps.keys();
      
      String[] toReturn = new String[serviceProps.size()];
      
      for(int i = 0 ; i < serviceProps.size(); i++)
        toReturn[i] = e.nextElement();
      
      return toReturn;
    }
    
    /**
     * @return the bundle this service reference was registered against.
     */
    public Bundle getBundle()
    {
      return registeringBundle;
    }
    
    /**
     * @return a service reference for this service registration.
     */
    public ServiceReference getReference()
    {
      return Skeleton.newMock(this, ServiceReference.class);
    }
    
    public Hashtable<String, Object> getProperties()
    {
      return new Hashtable<String, Object>(serviceProps);
    }

    /**
     * Implement the standard behaviour of the registry
     */
    public int compareTo(ServiceReference o) {
      Integer rank = (Integer) serviceProps.get(Constants.SERVICE_RANKING);
      if(rank == null)
        rank = 0;
      
      Integer otherRank = (Integer) o.getProperty(Constants.SERVICE_RANKING);
      if(otherRank == null)
        otherRank = 0;
      //Higher rank = higher order
      int result = rank.compareTo(otherRank);
      
      if(result == 0) {
        Long id = (Long) serviceProps.get(Constants.SERVICE_ID);
        Long otherId = (Long) o.getProperty(Constants.SERVICE_ID);
        //higher id = lower order
        return otherId.compareTo(id);
      }
      return result;
    }
  }

  /** The bundle associated with this bundle context */
  private Bundle bundle;

  /**
   * Default constructor, widely used in the tests.
   */
  public BundleContextMock()
  {
    bundle = Skeleton.newMock(new BundleMock("test." + new Random(System.currentTimeMillis()).nextInt(),
                              new Hashtable<Object, Object>()), Bundle.class);
  }
  
  /**
   * Constructor used by BundleMock, it ensures the bundle and its context are wired together correctly.
   * 
   * TODO We have to many Bundle mocks objects for a single OSGi bundle, we need to update this.
   * 
   * @param b
   */
  public BundleContextMock(Bundle b)
  {
    bundle = b;
  }
  
  /**
   * This checks that we have at least one service with this interface name.
   * 
   * @param interfaceName the name of the interface.
   */
  public static void assertServiceExists(String interfaceName)
  {
    assertTrue("No service registered with interface " + interfaceName + ". Services found: " + registry.keySet(), registry.containsKey(interfaceName));
  }
  
  /**
   * This checks that we have at no services with this interface name.
   * 
   * @param interfaceName the name of the interface.
   */
  public static void assertNoServiceExists(String interfaceName)
  {
    assertFalse("Services registered with interface " + interfaceName + ". Services found: " + registry.keySet(), registry.containsKey(interfaceName));
  }
  
  /**
   * This implements the registerService method from BundleContext.
   * 
   * @param interFace
   * @param service
   * @param properties
   * @return the ServiceRegistration object for this service.
   */
  public ServiceRegistration registerService(String interFace, final Object service, Dictionary<String, Object> properties)
  {
    // validate that the service implements interFace
    try {
      Class<?> clazz = Class.forName(interFace, false, service.getClass().getClassLoader());
      
      if (!clazz.isInstance(service) && !(service instanceof ServiceFactory)) {
        throw new AssertionFailedError("The service " + service + " does not implement " + interFace);
      }
    } catch (ClassNotFoundException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }

    return registerService(new String[] {interFace}, service, properties);
  }
  
  /**
   * This implements the registerService method from BundleContext.
   * 
   * @param interfaces
   * @param service
   * @param properties
   * @return the ServiceRegistration object for this service.
   */
  public ServiceRegistration registerService(String[] interfaces, Object service, Dictionary<String, Object> properties)
  {
    if (properties == null) properties = new Hashtable<String, Object>();
    
    ServiceData data = new ServiceData();
    // cast the service to a service factory because in our framework we only ever register
    // a service factory. If we every put a non-service factory object in that is a failure.
    properties.put(Constants.OBJECTCLASS, interfaces);
    properties.put(Constants.SERVICE_ID, nextId++);
    if (service instanceof ServiceFactory) {
      data.serviceImpl = (ServiceFactory)service;
    } else {
      data.serviceImpl = new MockServiceFactory(service);
    }
    data.interfaceNames = interfaces;
    data.registeringBundle = bundle;
    
    Enumeration<String> keys = properties.keys();
    
    while (keys.hasMoreElements()) {
      String key = keys.nextElement();
      data.serviceProps.put(key, properties.get(key));
    }
    
    data.register();
    
    return Skeleton.newMock(data, ServiceRegistration.class);
  }

  /**
   * This helper method is used to get the service from the registry with the
   * given interface name.
   * 
   * <p>This should really return multiple services.
   * </p>
   * 
   * @param interfaceName the interface name.
   * @param bundle        the bundle name.
   * @return the registered service.
   */
  public static Object getService(String interfaceName, Bundle bundle)
  {
    List<ServiceData> datum = registry.get(interfaceName);
    
    if (datum == null) return null;
    else if (datum.isEmpty()) return null;
    // this is safe for now, but may not be when we do other scoped components.
    else {
      ServiceRegistration reg = Skeleton.newMock(ServiceRegistration.class);
      return datum.iterator().next().serviceImpl.getService(bundle, reg);
    }
  }
  
  /**
   * A mock implementation of the getServiceReferences method. It does not currently
   * process the filter, this is probably a bit hard, so we might cheat when we do.
   * 
   * <p>Note this does not check that the service classes are visible to the
   *   caller as OSGi does. It is equivalent to getAllServiceReferences.
   * </p>
   * 
   * @param className the name of the class the lookup is for.
   * @param filter
   * @return an array of matching service references.
   * @throws InvalidSyntaxException
   */
  public ServiceReference[] getServiceReferences(String className, String filter) throws InvalidSyntaxException
  {
    List<ServiceData> data = new ArrayList<ServiceData>();
    
    if (className != null) {
      List<ServiceData> tmpData = registry.get(className);
      if (tmpData != null) data.addAll(tmpData);
    } else {
      data = new ArrayList<ServiceData>();
      for (List<ServiceData> value : registry.values())
      data.addAll(value);
    }
    
    ServiceReference[] refs;

    if (data == null) {
      refs = null;
    } else {
      
      if (filter != null) {
        Filter f = FrameworkUtil.createFilter(filter);
        
        Iterator<ServiceData> it = data.iterator();
        
        while (it.hasNext()) {
          ServiceData sd = it.next();
          
          if (!!!f.match(sd.getProperties())) it.remove();
        }
      }
      
      if (data.isEmpty()) return null;
      
      refs = new ServiceReference[data.size()];
      for (int i = 0; i < refs.length; i++) {
        refs[i] = Skeleton.newMock(data.get(i), ServiceReference.class);
      }
    }
    
    return refs;
  }
  
  /**
   * Gets the first matching service reference.
   * 
   * @param className the class name wanted.
   * @return the matchine service, or null if one cannot be found.
   */
  public ServiceReference getServiceReference(String className)
  {
    ServiceReference[] refs;
    try {
      refs = getServiceReferences(className, null);
      if (refs != null) return refs[0];
      
      return null;
    } catch (InvalidSyntaxException e) {
      // should never happen.
      e.printStackTrace();
    }
    return null;
  }
  
  /**
   * This method finds all the service references in the registry with the
   * matching class name and filter.
   * 
   * @param className
   * @param filter
   * @return the matching service references.
   * @throws InvalidSyntaxException
   */
  public ServiceReference[] getAllServiceReferences(String className, String filter) throws InvalidSyntaxException
  {
    return getServiceReferences(className, filter);
  }
  
  /**
   * Retrieve a service from the registry.
   * @param ref the service reference.
   * @return    the returned service.
   */
  public Object getService(ServiceReference ref)
  {
    ServiceData data = (ServiceData)Skeleton.getSkeleton(ref).getTemplateObject();
    
    return data.serviceImpl.getService(getBundle(), Skeleton.newMock(data, ServiceRegistration.class));
  }
  
  /**
   * This method implements the installBundle method from BundleContext. It
   * makes use of the java.util.jar package to parse the manifest from the input
   * stream.
   * 
   * @param location the location of the bundle.
   * @param is       the input stream to read from.
   * @return         the created bundle.
   * @throws BundleException
   */
  public Bundle installBundle(String location, InputStream is) throws BundleException
  {
    Bundle b;
    JarInputStream jis;
    try {
      jis = new JarInputStream(is);

      Manifest man = jis.getManifest();
      
      b = createBundle(man, null);
      
    } catch (IOException e) {
      throw new BundleException(e.getMessage(), e);
    }
    
    return b;
  }

  /**
   * Create a mock bundle correctly configured using the supplied manifest and
   * location.
   * 
   * @param man      the manifest to load.
   * @param location the location on disk.
   * @return the created bundle
   * @throws MalformedURLException
   */
  private Bundle createBundle(Manifest man, String location) throws MalformedURLException
  {
    Attributes attribs = man.getMainAttributes();
    String symbolicName = attribs.getValue(Constants.BUNDLE_SYMBOLICNAME);
    
    Hashtable<Object, Object> attribMap = new Hashtable<Object, Object>();
    
    for (Map.Entry<Object, Object> entry : attribs.entrySet()) {
      Attributes.Name name = (Attributes.Name)entry.getKey();
      attribMap.put(name.toString(), entry.getValue());
    }
    
    BundleMock mock = new BundleMock(symbolicName, attribMap, location);

    mock.addToClassPath(new File("build/unittest/classes").toURL());

    Bundle b = Skeleton.newMock(mock, Bundle.class);
    
    bundles.add(b);

    return b;
  }
  
  /**
   * Asks to install an OSGi bundle from the given location.
   * 
   * @param location the location of the bundle on the file system.
   * @return the installed bundle.
   * @throws BundleException
   */
  public Bundle installBundle(String location) throws BundleException
  {
    try {
      URI uri = new URI(location.replaceAll(" ", "%20"));

      File baseDir = new File(uri);
      Manifest man = null;
      //check if it is a directory
      if (baseDir.isDirectory()){
      man = new Manifest(new FileInputStream(new File(baseDir, "META-INF/MANIFEST.MF")));
      }
      //if it isn't assume it is a jar file
      else{
        InputStream is = new FileInputStream(baseDir);
        JarInputStream jis = new JarInputStream(is);
        man = jis.getManifest();
        jis.close();
        if (man == null){
          throw new BundleException("Null manifest");
        }
      }
      
      return createBundle(man, location);
    } catch (IOException e) {
      throw new BundleException(e.getMessage(), e);
    } catch (URISyntaxException e) {
      // TODO Auto-generated catch block
      throw new BundleException(e.getMessage(), e);
    }
  }
  
  /**
   * @return all the bundles in the system
   */
    public Bundle[] getBundles()
  {
    return bundles.toArray(new Bundle[bundles.size()]);
  }
  
  /**
   * Add a service listener.
   * 
   * @param listener
   * @param filter
   */
  public void addServiceListener(ServiceListener listener, String filter)
  {
    listeners.add(new FilteredServiceListener(filter, listener));
  }

  /**
   * Add a service listener.
   * 
   * @param listener
   */
  public void addServiceListener(ServiceListener listener) 
  {
    listeners.add(listener);
  }
  
  /**
   * Remove a service listener
   * @param listener
   */
  public void removeServiceListener(ServiceListener listener)
  {
    listeners.remove(new FilteredServiceListener(null, listener));
  }
  
  public String getProperty(String name)
  {
    if (Constants.FRAMEWORK_VERSION.equals(name)) {
      return "4.1";
    }
    /*added System.getProperty so that tests can set a system property
     * but it is retrieved via the BundleContext.
     * This allows tests to emulate different properties being set on the
     * context, helpful for the feature pack launcher/kernel relationship
     */
    return System.getProperty(name);
  }
  
  /**
   * @return the bundle associated with this bundle context (if we created one).
   */
  public Bundle getBundle()
  {
    return bundle;
  }
  
  /**
   * This method clears the service registry.
   */
  public static void clear()
  {
    registry.clear();
    bundles.clear();
    listeners.clear();
    nextId = 0;
  }
  
  public static List<ServiceListener> getServiceListeners()
  {
    return listeners;
  }

  public void addBundle(Bundle b)
  {
    bundles.add(b);
  }
}