/*
 * 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.brooklyn.core.entity.drivers;

import static com.google.common.base.Preconditions.checkNotNull;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.drivers.DriverDependentEntity;
import org.apache.brooklyn.api.entity.drivers.EntityDriver;
import org.apache.brooklyn.api.entity.drivers.EntityDriverManager;
import org.apache.brooklyn.api.location.Location;

import com.google.common.base.Objects;
import com.google.common.base.Throwables;

/**
 * A registry of driver classes, keyed off the driver-interface + location type it is for.
 * 
 * @author Aled Sage
 */
public class RegistryEntityDriverFactory implements EntityDriverManager {

    private final Map<DriverLocationTuple, Class<? extends EntityDriver>> registry = new LinkedHashMap<DriverLocationTuple, Class<? extends EntityDriver>>();

    @Override
    public <D extends EntityDriver> D build(DriverDependentEntity<D> entity, Location location) {
        Class<? extends D> driverClass = lookupDriver(entity.getDriverInterface(), location);
        return newDriverInstance(driverClass, entity, location);
    }

    public boolean hasDriver(DriverDependentEntity<?> entity, Location location) {
        return lookupDriver(entity.getDriverInterface(), location) != null;
    }

    @Override
    public <D extends EntityDriver> void registerDriver(Class<D> driverInterface, Class<? extends Location> locationClazz, Class<? extends D> driverClazz) {
        synchronized (registry) {
            registry.put(new DriverLocationTuple(driverInterface, locationClazz), driverClazz);
        }
    }

    @SuppressWarnings("unchecked")
    private <D extends EntityDriver> Class<? extends D> lookupDriver(Class<D> driverInterface, Location location) {
        synchronized (registry) {
            for (DriverLocationTuple contender : registry.keySet()) {
                if (contender.matches(driverInterface, location)) {
                    return (Class<? extends D>) registry.get(contender);
                }
            }
        }
        return null;
    }
    
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private <D> Constructor<D> getConstructor(Class<? extends D> driverClass) {
        for (Constructor constructor : driverClass.getConstructors()) {
            if (constructor.getParameterTypes().length == 2) {
                return constructor;
            }
        }

        //TODO:
        throw new RuntimeException(String.format("Class [%s] has no constructor with 2 arguments",driverClass.getName()));
    }

    private <D> D newDriverInstance(Class<D> driverClass, Entity entity, Location location) {
        Constructor<D> constructor = getConstructor(driverClass);
        try {
            constructor.setAccessible(true);
            return constructor.newInstance(entity, location);
        } catch (InstantiationException e) {
            throw Throwables.propagate(e);
        } catch (IllegalAccessException e) {
            throw Throwables.propagate(e);
        } catch (InvocationTargetException e) {
            throw Throwables.propagate(e);
        }
    }

    private static class DriverLocationTuple {
        private final Class<? extends EntityDriver> driverInterface;
        private final Class<? extends Location> locationClazz;
        
        public DriverLocationTuple(Class<? extends EntityDriver> driverInterface, Class<? extends Location> locationClazz) {
            this.driverInterface = checkNotNull(driverInterface, "driver interface");
            this.locationClazz = checkNotNull(locationClazz, "location class");
        }
        
        public boolean matches(Class<? extends EntityDriver> driver, Location location) {
            return driverInterface.isAssignableFrom(driver) && locationClazz.isInstance(location);
        }
        
        @Override
        public int hashCode() {
            return Objects.hashCode(driverInterface, locationClazz);
        }
        
        @Override
        public boolean equals(Object other) {
            if (!(other instanceof DriverLocationTuple)) {
                return false;
            }
            DriverLocationTuple o = (DriverLocationTuple) other;
            return driverInterface.equals(o.driverInterface) && locationClazz.equals(o.locationClazz);
        }
    }
}
