/*
 * Copyright (C) 2012 Darth Affe <http://wyrez.org> and contributors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.wyrez.persij;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Darth Affe
 */
final class PersiJMapping {

    private static final Logger logger = Logger.getLogger("org.wyrez.persij");
    private Object singletonInstance;
    private Class type;
    private boolean singleton;

    PersiJMapping(Class type, boolean singleton) {
        this.type = type;
        this.singleton = singleton;
    }

    PersiJMapping(Class type, Object singleton) {
        this.type = type;
        this.singleton = true;
        singletonInstance = singleton;
    }

    final boolean isSingleton() {
        return singleton;
    }

    /**
     * Creates a new instance
     *
     * @param container Container to resolve missing dependencies
     * @param paramMapping Optional parameters used to resolve dependencies
     * (null if not used)
     * @return New instance
     * @throws NoSuitableConstructorFoundException thrown if no suitable
     * constructor to instantiate could be found
     */
    final Object getInstance(PersiJContainer container, PersiJParamMapping paramMapping) throws NoSuitableConstructorFoundException {
        if (isSingleton()) {
            if (singletonInstance == null) {
                singletonInstance = createInstance(container, paramMapping);
            }
            return singletonInstance;
        } else {
            return createInstance(container, paramMapping);
        }
    }

    /**
     * Creates an array with new instances
     *
     * @param container Container to resolve missing dependencies
     * @param paramMapping Optional parameters used to resolve dependencies
     * (null if not used)
     * @param array Array which should be filled and returned
     * @return New array with instances
     * @throws NoSuitableConstructorFoundException thrown if no suitable
     * constructor to instantiate could be found
     */
    final Object getInstances(PersiJContainer container, PersiJParamMapping paramMapping, Object[] array) throws NoSuitableConstructorFoundException {
        if (isSingleton()) {
            if (singletonInstance == null) {
                singletonInstance = createInstance(container, paramMapping);
            }
            for (int i = 0; i < array.length; i++) {
                array[i] = singletonInstance;
            }
            return array;
        } else {
            return createInstances(container, paramMapping, array);
        }
    }

    /**
     * Creates a new instance
     *
     * @param container Container to resolve missing dependencies
     * @param paramMapping Optional parameters used to resolve dependencies
     * (null if not used)
     * @return New instance
     * @throws NoSuitableConstructorFoundException thrown if no suitable
     * constructor to instantiate could be found
     */
    private Object createInstance(PersiJContainer container, PersiJParamMapping paramMapping) throws NoSuitableConstructorFoundException {
        Constructor constructor = getBestConstructor(container, paramMapping);
        return createInstance(container, constructor, paramMapping);
    }

    /**
     * Creates an array with new instances
     *
     * @param container Container to resolve missing dependencies
     * @param paramMapping Optional parameters used to resolve dependencies
     * (null if not used)
     * @param array Array which should be filled and returned
     * @return New array with instances
     * @throws NoSuitableConstructorFoundException thrown if no suitable
     * constructor to instantiate could be found
     */
    private Object createInstances(PersiJContainer container, PersiJParamMapping paramMapping, Object[] array) throws NoSuitableConstructorFoundException {
        Constructor constructor = getBestConstructor(container, paramMapping);
        for (int i = 0; i < array.length; i++) {
            array[i] = createInstance(container, constructor, paramMapping);
        }
        return array;
    }

    /**
     * Creates an instance using the given constructor
     *
     * @param container Container to resolve missing dependencies
     * @param constructor Constructor to instantiate
     * @param paramMapping Optional parameters used to resolve dependencies
     * (null if not used)
     * @return New instance
     * @throws NoSuitableConstructorFoundException thrown if no suitable
     * constructor to instantiate could be found
     */
    private Object createInstance(PersiJContainer container, Constructor constructor, PersiJParamMapping paramMapping) throws NoSuitableConstructorFoundException {
        Object[] params;
        if (paramMapping != null && paramMapping.getType() == type) {
            params = paramMapping.getParams();
        } else {
            Class[] conParams = constructor.getParameterTypes();
            int paramCount = conParams.length;
            params = new Object[paramCount];
            for (int i = 0; i < paramCount; i++) {
                params[i] = container.resolve(conParams[i], paramMapping);
            }
        }
        try {
            return constructor.newInstance(params);
        } catch (Exception ex) {
            logger.log(Level.WARNING, "Error while creating instance of an object", ex);
            return null;
        }
    }

    /**
     * Determines the best constructor to instantiate
     *
     * @param container Container to check registered types
     * @param paramMapping Optional parameters used to resolve dependencies
     * (null if not used)
     * @return Best constructor to instantiate
     * @throws NoSuitableConstructorFoundException thrown if no suitable
     * constructor to instantiate could be found
     */
    private Constructor getBestConstructor(PersiJContainer container, PersiJParamMapping paramMapping) throws NoSuitableConstructorFoundException {
        Constructor bestToCreate = null;
        int paramCount = -1;
        for (Constructor c : type.getConstructors()) {
            Annotation annotation = c.getAnnotation(PersiJConstructor.class);
            if (paramMapping != null && paramMapping.getType() == type) {
                if (annotation != null && ((PersiJConstructor) annotation).customParamMappingId() == paramMapping.getParamMappingId()) {
                    bestToCreate = c;
                    break;
                }
            } else {
                boolean possible = true;
                for (Class t : c.getParameterTypes()) {
                    if (t.isPrimitive() || t == String.class || t == Object.class || !container.isTypeRegistered(t)) {
                        possible = false;
                        break;
                    }
                }
                if (possible) {
                    int pCount = c.getParameterTypes().length;
                    if (annotation != null) {
                        if (((PersiJConstructor) annotation).isDefault()) {
                            bestToCreate = c;
                            break;
                        }
                    } else if (pCount > paramCount) {
                        bestToCreate = c;
                        paramCount = pCount;
                    }
                }
            }
        }
        if (bestToCreate == null) {
            throw new NoSuitableConstructorFoundException();
        }
        return bestToCreate;
    }
}
