/*
 * Copyright (c) 2004-2017, by Alex Leigh.
 * All rights reserved.
 *
 * THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE
 * The copyright notice above does not evidence any
 * actual or intended publication of such source code.
 */

package lc.mecha.lang;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * The <code>Instancer</code> provides more robust support for dynamically instantiating classes. It supports
 * automatically matching constructors to argument stacks, as well as supports an object reference cache
 * that allows <code>Pointer</code> object present in an argument stack to be dynamically replaced with
 * a previouslly instantiated object.
 *
 * @author Alex Leigh
 */
final class Instancer {
    private static final Logger logger = LoggerFactory.getLogger(Instancer.class);
    private final HashMap<Pointer, Object> references = new HashMap<>();

    public void addObjectReference(final Pointer ptr, final Object ref) {
        references.put(ptr, ref);
    }

    /**
     * This method attempts to dynamically instantiate a new object of the given
     * class name using the given argument stack for the constructor.
     * <p>
     * If the class cannot be found, or can be found and a matching constructor
     * can be found, an exception is thrown. Otherwise, the newly instantiated object
     * is returned.
     * <p>
     * <code>Pointer</code> objects in the parameter stack will be replaced with their
     * corresponding objects from the Instancer references cache. If no matching reference
     * is found, an exception is thrown.
     *
     * @param className
     * @param parameters
     * @return
     * @throws ClassNotFoundException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */

    public Object loadClass(final String className, final Object... parameters) throws ClassNotFoundException,
            InvocationTargetException, IllegalAccessException, InstantiationException {
        final Class c = Instancer.class.getClassLoader().loadClass(className);

        final Constructor foundCon = findConstructor(c, parameters);

        return foundCon.newInstance(parameters);
    }

    /**
     * For the given class <code>c</code> and the given <code>parameters</code>, attempt to
     * locate a matching constructor. Constructors will be evaluated in the order they are
     * returned via reflection.
     * <p>
     * If no matching constructor is found, or there were other problems, an exception is
     * thrown, otherwise, the matching constructor is returned.
     * <p>
     * <code>Pointer</code> objects in the parameter stack will be replaced with their
     * corresponding objects from the Instancer references cache. If no matching reference
     * is found, an exception is thrown.
     *
     * @param c
     * @param parameters
     * @return
     * @throws InstantiationException
     */
    public Constructor findConstructor(final Class c, final Object... parameters) throws
            InstantiationException {
        final List<Class> classTypes = new ArrayList<>();
        for (int i = 0; i < parameters.length; i++) {
            if (parameters[i] instanceof Pointer) {
                final Object o = references.get(parameters[i]);
                if (o == null)
                    throw new InstantiationException("Referenced object not found: " + parameters[i]);
                parameters[i] = o;
            }
            classTypes.add(parameters[i].getClass());
        }

        logger.debug("Provided argument types are: {}", classTypes);

        final Constructor[] cons = c.getConstructors();

        for (final Constructor con : cons) {
            logger.debug("Evaluating constructor {}", con);
            final Class[] vars = con.getParameterTypes();
            if (vars.length != classTypes.size()) {
                logger.debug("Invalid lengths. Skipping this constructor. {} {}",
                        new Object[]{Integer.valueOf(vars.length), Integer.valueOf(classTypes.size())});
                continue;
            }

            boolean matched = true; // Optimism

            logger.debug("Eval: {}", con);

            for (int i = 0; i < vars.length; i++) {
                logger.debug("Comparing: {} {} {}", vars[i], classTypes.get(i),
                        Boolean.valueOf(vars[i].isInstance(classTypes.get(i))));

                if (!vars[i].isAssignableFrom(classTypes.get(i)))
                    matched = false;
            }

            logger.debug("Matched constructor: {} {}", new Object[]{Boolean.valueOf(matched), con});

            if (matched) {
                return con;
            }
        }

        throw new InstantiationException("No matching constructor found.");
    }
}
